/* * Seldon -- open source prediction engine * ======================================= * * Copyright 2011-2015 Seldon Technologies Ltd and Rummble Ltd (http://www.seldon.io/) * * ******************************************************************************************** * * 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. * * ******************************************************************************************** */ /* * Created on 09-May-2006 * */ package io.seldon.db.jdo; import java.sql.Connection; import java.sql.SQLException; import java.util.Collection; import java.util.HashSet; import javax.jdo.JDODataStoreException; import javax.jdo.JDOException; import javax.jdo.PersistenceManager; import javax.jdo.datastore.JDOConnection; import org.apache.log4j.Logger; import org.datanucleus.api.jdo.JDOTransaction; /** * Handle Transactions in JDO (for mysql) * <ul> * <li> Ensures a Read-Only transaction is used whne not inside Transction class. This allows mysql ReplicationDriver * to send these commands to Slave mysql instances. Inside a Transaction commands will be sent to master for * writing. * <li> Ensures dead-lock exceptions are caught and the transaction tried a again a set number of times before failing. * <li> Allows nested transactions * <li> Throws warnings if a write is done inside a read transaction * * </ul> * @author rummble * */ public class TransactionPeer { private static Logger logger = Logger.getLogger( TransactionPeer.class.getName() ); private static final int DEADLOCK_RETRIES = 5; // taken from mysql connector/j example - dunno if sensible //private static JDOLifecycleListener JDODirtyListener = new JDOLifecycleListener(); private static NestedTransactionCounter nesting = new NestedTransactionCounter(); public static void resetNestingCounter() { nesting.set(null); } public static void runTransaction(Transaction t) throws DatabaseException { runTransaction(new HashSet(),t); } public static void runTransaction(Collection members,Transaction t) throws DatabaseException { boolean success = false; boolean topLevelTransaction = nesting.startTransaction() == 1; try { for (int tries=0;!success && tries<DEADLOCK_RETRIES;tries++) { try { if (topLevelTransaction) { PersistenceManager pm = t.getPersistenceManager(); closeReadOnlyTransaction(pm); if (members.isEmpty()) TransactionPeer.startTransaction(t.getPersistenceManager()); //else // TransactionPeer.startTransaction(members); } else { // add locks for members. Its ok if these have been done already. //if (!members.isEmpty()) //{ // PersistenceManager pm = JDOFactory.getNonTransactionalPersistenceManager(); // lockMembers(pm,members); //} } t.process(); if (topLevelTransaction) TransactionPeer.commitTransaction(t.getPersistenceManager()); success = true; } catch (JDODataStoreException ex) { //logger.warn("Caught jdo datastore exception",ex); Throwable[] nested = ex.getNestedExceptions (); for (int i = 0; nested != null && i < nested.length; i++) { SQLException sqlEx = null; if (nested[i] instanceof SQLException) sqlEx = (SQLException) nested[i]; if (sqlEx != null) { switch(SQLErrorPeer.diagnoseSQLError(sqlEx)) { case SQLErrorPeer.SQL_DUPLICATE_KEY: throw new DatabaseException(SQLErrorPeer.SQL_DUPLICATE_KEY); case SQLErrorPeer.SQL_DEADLOCK: logger.warn("Caught deadlock exception on attempt: " + tries); break; case SQLErrorPeer.SQL_LOCK_TIMEOUT: logger.warn("Caught lock wait timeout on attempt: " + tries); break; case SQLErrorPeer.SQL_NETWORK_PROBLEM: logger.warn("Caught comms error on attempt: " + tries); break; default: { logger.error("Caught unknown JDO data store exception",sqlEx); throw new DatabaseException(SQLErrorPeer.SQL_ERROR,ex); } } } else logger.error("Unknown nested exception in JDO exception, nested["+i+"]",nested[i]); } } catch (JDOException ex) { logger.error("Caught unhandled JDO exception",ex); throw new DatabaseException(SQLErrorPeer.SQL_ERROR,ex); } catch (SQLException ex) { switch(SQLErrorPeer.diagnoseSQLError(ex)) { case SQLErrorPeer.SQL_DUPLICATE_KEY: throw new DatabaseException(SQLErrorPeer.SQL_DUPLICATE_KEY); case SQLErrorPeer.SQL_DEADLOCK: logger.warn("Caught deadlock exception on attempt: " + tries); break; case SQLErrorPeer.SQL_LOCK_TIMEOUT: logger.warn("Caught lock wait timeout on attempt: " + tries); break; case SQLErrorPeer.SQL_NETWORK_PROBLEM: logger.warn("Caught comms error on attempt: " + tries); break; default: { logger.error("Caught unknown database error:",ex); throw new DatabaseException(SQLErrorPeer.SQL_ERROR,ex); } } } catch (NestedTransactionException ex) { if (!topLevelTransaction) throw new NestedTransactionException(); } catch (DatabaseException ex) { logger.info("Caught playtxt exception ",ex); throw ex; } catch (Exception ex) { logger.error("Caught exception during transaction:",ex); throw new DatabaseException(-1,ex); } // catch (PlaytxtException ex) // { // logger.info("Caught playtxt exception ",ex); // //PersistenceManager pm = JDOFactory.getNonTransactionalPersistenceManager(); // //pm.currentTransaction().rollback(); // } finally { if (topLevelTransaction) { rollbackIfActive(t.getPersistenceManager()); PersistenceManager pm = t.getPersistenceManager(); startReadOnlyTransaction(pm); } else { if (!success) { logger.warn("Nested Transaction failed"); throw new NestedTransactionException(); } } } } if (!success) { final String message = "Failed to carry out transaction"; logger.error(message, new Exception(message)); throw new DatabaseException(-1); } } finally { nesting.endTransaction(); } } public static void startReadOnlyTransaction(PersistenceManager pm) { if (!pm.currentTransaction().isActive()) { // Check to ensure we never write in a readOnly transaction. // as should never be called shouldn't use up much extra CPU cycles if any //pm.addInstanceLifecycleListener(JDODirtyListener,(Class[]) null); //((UserTransaction)pm.currentTransaction()).setUseUpdateLock(false); JDOTransaction tx = (JDOTransaction)pm.currentTransaction(); //tx.setOption("transaction.serializeReadObjects", false); tx.setOption("datanucleus.SerializeRead", false); pm.currentTransaction().begin(); try { setMySQLReadOnly(pm,true); } catch (SQLException ex) { logger.error("Caught unexpected SQLException trying to set ReadOnly true", ex); SQLErrorPeer.diagnoseSQLError(ex); // What to do now? Need to handle the infamous "communications link error" here? } } } public static void closeReadOnlyTransaction(PersistenceManager pm) throws SQLException { if (pm.currentTransaction().isActive()) { if (!isMySQLReadOnly(pm)) logger.warn("We are not in a read-only transaction"); // Check to ensure we never write in a readOnly transaction. /* if (JDODirtyListener.isDirty()) { final String message = "Readonly transaction is dirty"; logger.error(message, new Exception(message)); JDODirtyListener.clear(); //throw new PlaytxtFatalException(Errors.JDO_WRITE_IN_READONLY_TX); } pm.removeInstanceLifecycleListener(JDODirtyListener); */ pm.currentTransaction().rollback(); } } private static boolean isMySQLReadOnly(PersistenceManager pm) throws SQLException { JDOConnection jdoconn = pm.getDataStoreConnection(); try { Connection conn = (Connection) jdoconn.getNativeConnection(); //com.mysql.jdbc.ReplicationConnection mySqlConn = (com.mysql.jdbc.ReplicationConnection) conn; Boolean val = conn.isReadOnly(); return val; } finally { jdoconn.close(); } } private static void setMySQLReadOnly(PersistenceManager pm,boolean readOnly) throws SQLException { JDOConnection jdoconn = pm.getDataStoreConnection(); try { Connection conn = (Connection) jdoconn.getNativeConnection(); //com.mysql.jdbc.ReplicationConnection mySqlConn = (com.mysql.jdbc.ReplicationConnection) conn; conn.setReadOnly(readOnly); String catalog = ((org.datanucleus.api.jdo.JDOPersistenceManagerFactory) pm.getPersistenceManagerFactory()).getCatalog(); conn.setCatalog(catalog); } finally { jdoconn.close(); } } private static void startTransaction(PersistenceManager pm) throws SQLException { //((UserTransaction)pm.currentTransaction()).setUseUpdateLock(true); JDOTransaction tx = (JDOTransaction)pm.currentTransaction(); //tx.setOption("transaction.serializeReadObjects", true); tx.setOption("datanucleus.SerializeRead", true); pm.currentTransaction().begin(); setMySQLReadOnly(pm,false); } private static void commitTransaction(PersistenceManager pm) throws SQLException { pm.currentTransaction().commit(); } public static void rollbackIfActive(PersistenceManager pm) { if (pm.currentTransaction().isActive()) { Throwable t = new Throwable(); t.fillInStackTrace(); logger.error("Rolling back a transaction that was still active",t); try { pm.currentTransaction().rollback(); } catch (Exception ex) { logger.error("Unexpected exception on roll back",ex); } } } }