/*
* Copyright (c) 2010-2016 Evolveum
*
* Licensed 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 com.evolveum.midpoint.repo.sql.helpers;
import com.evolveum.midpoint.repo.sql.*;
import com.evolveum.midpoint.repo.sql.util.RUtil;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import org.apache.commons.lang.StringUtils;
import org.hibernate.*;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.jdbc.Work;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.SQLException;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_EXP_THRESHOLD;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_MAX_ATTEMPTS;
import static com.evolveum.midpoint.repo.sql.SqlBaseService.LOCKING_TIMEOUT_STEP;
/**
* Core functionality needed in all members of SQL service family.
* Taken out of SqlBaseService in order to be accessible from other helpers without having to autowire SqlRepositoryServiceImpl
* (as it causes problems with Spring AOP proxies.)
*
* @author lazyman
* @author mederly
*/
@Component
public class BaseHelper {
private static final Trace LOGGER = TraceManager.getTrace(BaseHelper.class);
@Autowired
private SessionFactory sessionFactory;
@Autowired
private SqlRepositoryFactory repositoryFactory;
@Autowired
private LocalSessionFactoryBean sessionFactoryBean;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
RUtil.fixCompositeIDHandling(sessionFactory);
this.sessionFactory = sessionFactory;
}
public LocalSessionFactoryBean getSessionFactoryBean() {
return sessionFactoryBean;
}
public Session beginReadOnlyTransaction() {
return beginTransaction(getConfiguration().isUseReadOnlyTransactions());
}
public Session beginTransaction() {
return beginTransaction(false);
}
public Session beginTransaction(boolean readOnly) {
Session session = getSessionFactory().openSession();
session.beginTransaction();
if (getConfiguration().getTransactionIsolation() == TransactionIsolation.SNAPSHOT) {
LOGGER.trace("Setting transaction isolation level SNAPSHOT.");
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
connection.createStatement().execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT");
}
});
}
if (readOnly) {
// we don't want to flush changes during readonly transactions (they should never occur,
// but if they occur transaction commit would still fail)
session.setFlushMode(FlushMode.MANUAL);
LOGGER.trace("Marking transaction as read only.");
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
connection.createStatement().execute("SET TRANSACTION READ ONLY");
}
});
}
return session;
}
public SqlRepositoryConfiguration getConfiguration() {
return repositoryFactory.getSqlConfiguration();
}
public void rollbackTransaction(Session session) {
rollbackTransaction(session, null, null, false);
}
public void rollbackTransaction(Session session, Exception ex, OperationResult result, boolean fatal) {
String message = ex != null ? ex.getMessage() : "null";
rollbackTransaction(session, ex, message, result, fatal);
}
public void rollbackTransaction(Session session, Exception ex, String message, OperationResult result,
boolean fatal) {
if (StringUtils.isEmpty(message) && ex != null) {
message = ex.getMessage();
}
// non-fatal errors will NOT be put into OperationResult, not to confuse the user
if (result != null && fatal) {
result.recordFatalError(message, ex);
}
if (session == null || session.getTransaction() == null || !session.getTransaction().isActive()) {
return;
}
session.getTransaction().rollback();
}
public void cleanupSessionAndResult(Session session, OperationResult result) {
if (session != null && session.isOpen()) {
session.close();
}
if (result != null && result.isUnknown()) {
result.computeStatus();
}
}
public void handleGeneralException(Exception ex, Session session, OperationResult result) {
if (ex instanceof RuntimeException) {
handleGeneralRuntimeException((RuntimeException) ex, session, result);
} else {
handleGeneralCheckedException(ex, session, result);
}
throw new IllegalStateException("Shouldn't get here"); // just a marker to be obvious that this method never returns normally
}
public void handleGeneralRuntimeException(RuntimeException ex, Session session, OperationResult result) {
LOGGER.debug("General runtime exception occurred.", ex);
if (isExceptionRelatedToSerialization(ex)) {
rollbackTransaction(session, ex, result, false);
// this exception will be caught and processed in logOperationAttempt,
// so it's safe to pass any RuntimeException here
throw ex;
} else {
rollbackTransaction(session, ex, result, true);
if (ex instanceof SystemException) {
throw (SystemException) ex;
} else {
throw new SystemException(ex.getMessage(), ex);
}
}
}
public void handleGeneralCheckedException(Exception ex, Session session, OperationResult result) {
LOGGER.error("General checked exception occurred.", ex);
boolean fatal = !isExceptionRelatedToSerialization(ex);
rollbackTransaction(session, ex, result, fatal);
throw new SystemException(ex.getMessage(), ex);
}
public int logOperationAttempt(String oid, String operation, int attempt, RuntimeException ex,
OperationResult result) {
boolean serializationException = isExceptionRelatedToSerialization(ex);
if (!serializationException) {
// to be sure that we won't miss anything related to deadlocks, here is an ugly hack that checks it (with some probability...)
boolean serializationTextFound = ex.getMessage() != null && (exceptionContainsText(ex, "deadlock") || exceptionContainsText(ex, "could not serialize access"));
if (serializationTextFound) {
LOGGER.error("Transaction serialization-related problem (e.g. deadlock) was probably not caught correctly!", ex);
}
throw ex;
}
double waitTimeInterval = LOCKING_TIMEOUT_STEP * Math.pow(2, attempt > LOCKING_EXP_THRESHOLD ? LOCKING_EXP_THRESHOLD : (attempt - 1));
long waitTime = Math.round(Math.random() * waitTimeInterval);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Waiting: attempt = " + attempt + ", waitTimeInterval = 0.." + waitTimeInterval + ", waitTime = " + waitTime);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("A serialization-related problem occurred when {} object with oid '{}', retrying after "
+ "{}ms (this was attempt {} of {})\n{}: {}", new Object[]{operation, oid, waitTime,
attempt, LOCKING_MAX_ATTEMPTS, ex.getClass().getSimpleName(), ex.getMessage()});
}
if (attempt >= LOCKING_MAX_ATTEMPTS) {
LOGGER.error("A serialization-related problem occurred, maximum attempts (" + attempt + ") reached.", ex);
if (ex != null && result != null) {
result.recordFatalError("A serialization-related problem occurred.", ex);
}
throw new SystemException(ex.getMessage() + " [attempts: " + attempt + "]", ex);
}
if (waitTime > 0) {
try {
Thread.sleep(waitTime);
} catch (InterruptedException ex1) {
// ignore this
}
}
return ++attempt;
}
private boolean isExceptionRelatedToSerialization(Exception ex) {
boolean rv = isExceptionRelatedToSerializationInternal(ex);
LOGGER.trace("Considering if exception {} is related to serialization: returning {}", ex, rv, ex);
return rv;
}
private boolean isExceptionRelatedToSerializationInternal(Exception ex) {
if (ex instanceof SerializationRelatedException
|| ex instanceof PessimisticLockException
|| ex instanceof LockAcquisitionException
|| ex instanceof HibernateOptimisticLockingFailureException
|| ex instanceof StaleObjectStateException) { // todo the last one is questionable
return true;
}
// it's not locking exception (optimistic, pesimistic lock or simple lock acquisition) understood by hibernate
// however, it still could be such exception... wrapped in e.g. TransactionException
// so we have a look inside - we try to find SQLException there
SQLException sqlException = findSqlException(ex);
if (sqlException == null) {
return false;
}
// these error codes / SQL states we consider related to locking:
// code 50200 [table timeout lock in H2, 50200 is LOCK_TIMEOUT_1 error code]
// code 40001 [DEADLOCK_1 in H2]
// state 40001 [serialization failure in PostgreSQL - http://www.postgresql.org/docs/9.1/static/transaction-iso.html - and probably also in other systems]
// state 40P01 [deadlock in PostgreSQL]
// code ORA-08177: can't serialize access for this transaction in Oracle
// code ORA-01466 ["unable to read data - table definition has changed"] in Oracle
// code ORA-01555: snapshot too old: rollback segment number with name "" too small
// code ORA-22924: snapshot too old
//
// sql states should be somewhat standardized; sql error codes are vendor-specific
// todo: so it is probably not very safe to test for codes without testing for specific database (h2, oracle)
// but the risk of problem is quite low here, so let it be...
// strange exception occurring in MySQL when doing multithreaded org closure maintenance
// alternatively we might check for error code = 1030, sql state = HY000
// but that would cover all cases of "Got error XYZ from storage engine"
if ((getConfiguration().isUsingMySQL() || getConfiguration().isUsingMariaDB())
&& sqlException.getMessage() != null
&& sqlException.getMessage().contains("Got error -1 from storage engine")) {
return true;
}
return sqlException.getErrorCode() == 50200
|| sqlException.getErrorCode() == 40001
|| "40001".equals(sqlException.getSQLState())
|| "40P01".equals(sqlException.getSQLState())
|| sqlException.getErrorCode() == 8177
|| sqlException.getErrorCode() == 1466
|| sqlException.getErrorCode() == 1555
|| sqlException.getErrorCode() == 22924
|| sqlException.getErrorCode() == 3960; // Snapshot isolation transaction aborted due to update conflict.
}
public SQLException findSqlException(Throwable ex) {
while (ex != null) {
if (ex instanceof SQLException) {
return (SQLException) ex;
}
ex = ex.getCause();
}
return null;
}
private boolean exceptionContainsText(Throwable ex, String text) {
while (ex != null) {
if (ex.getMessage() != null && ex.getMessage().contains(text)) {
return true;
}
ex = ex.getCause();
}
return false;
}
}