/*
* This file is part of the OpenNMS(R) Application.
*
* OpenNMS(R) is Copyright (C) 2011 The OpenNMS Group, Inc. All rights reserved.
* OpenNMS(R) is a derivative work, containing both original code, included code and modified
* code that was published under the GNU General Public License. Copyrights for modified
* and included code are below.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* For more information contact:
* OpenNMS Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*/
package org.opennms.netmgt.dao.support;
import org.opennms.netmgt.dao.OnmsDao;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
/**
* A UpsertTemplate for handling the update if it exists/insert if it doesn't case in the midst of
* concurrent database updates. The pattern for solving this is known as an Upsert.. update or insert.
*
* Example Scenario: During a node scan an interface is found on a node and various information about
* this interface is gathered. This information needs to be persisted to the database. There are two
* cases:
*
* 1. The interface is not yet in the database so it needs to be inserted
* 2. The interface is already in the database so it needs to be updated
*
* The naive implementation of this is something like the following (this is just pseudo code)
*
* // find an interface in the db matching our scanned info
* SnmpInterface dbIf = m_dao.query(scannedIf);
* if (dbIf != null) {
* // found an if in the db... updated with found info
* retrun update(dbIf, scannedIf);
* } else {
* // no if in the db.. insert the one we found
* return insert(scannedIf);
* }
*
* Problem: The naive implementation above has the problem that it fails in the midst of concurrency.
* Consider the following scenario where two different provisioning threads decide to update/insert
* the same interface that does not yet exist in the db:
*
* 1 Thread 1 attempts to find the if and finds it is not there
* 2 Thread 2 attempts to find the if and finds it is not there
* 3 Thread 1 inserts the if into the database
* 4 Thread 1 completes and moves onto further work
* 5 Thread 2 attempts to insert the if into the database and a duplication exception is thrown
* 6 All work done in Thread 2's transactions is rolled back.
*
* Most people assume the 'transactions' will handle this case but they do not. The reason for this is
* because transactions lock the information that is found to ensure that this information is not changed
* by others. However when you perform a query that returns nothing there is nothing to lock. So this case
* is not protected by the transaction.
*
* Solution: To handle this we must execute the insert in a 'sub transaction' and retry in the event of failure: The
* basic loop looks something like the following pseudo code:
*
* while(true) {
* SnmpInterface dbIf = m_dao.query(scannedIf);
* if (dbIf != null) {
* return update(dbIf, scannedIf);
* } else {
* try {
* // start a new sub transaction here that rolls back on exception
* return insert(scannedIf)
* } catch(Exception e) {
* // log failure and let the loop retry
* }
* }
* }
*
* This is simplified loop because it does not show all of the code to start transactions and such nor to it show
* the real.
*
* As far as code goes this solution has a great deal of boiler plate code. This class contains this boilerplate
* code using the Template Method pattern and provides abstract methods for filling running the query and for doing the
* insert and/or the update. To use this class to do the above would look something like this:
*
* final SnmpInterface scannedIf = ...;
* return UpsertTemplate<SnmpInterface>(transactionManager) {
* @Override
* public SnmpInterface query() {
* return m_dao.query(scannedIf);
* }
* @Override
* public SnmpInterface doUpdate(SnmpInterface dbIf) {
* return update(dbIf, scannedIf);
* }
* public SnmpInterface doInsert() {
* return insert(scannedIf);
* }
*
* }.execute();
*
* The above will handle all of the exceptions that can occur in the face of concurrent inserts and will properly either
* insert or update the interface.
*
*
* @author brozow
*/
public abstract class UpsertTemplate<T, D extends OnmsDao<T, ?>> {
protected final PlatformTransactionManager m_transactionManager;
protected final D m_dao;
/**
* Create an UpsertTemplate using the PlatformTransactionManager for creating
* transactions. This will retry a failed insert no more than two times.
*/
public UpsertTemplate(PlatformTransactionManager transactionManager, D dao) {
m_transactionManager = transactionManager;
m_dao = dao;
}
/**
* After creating the UpsertTemplate call this method to attempt the upsert.
*/
public T execute() {
TransactionTemplate template = new TransactionTemplate(m_transactionManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return template.execute(new TransactionCallback<T>() {
@Override
public T doInTransaction(TransactionStatus status) {
return doUpsert();
}
});
}
/**
* Called from upsert after it creates a transaction.
*/
private T doUpsert() {
T dbObj = query();
if (dbObj != null) {
// if we found the object then update and return
return update(dbObj);
}
// lock the table since we are about to insert and don't want it inserted
m_dao.lock();
// make sure it wasn't inserted while we waited for the lock
dbObj = query();
if (dbObj != null) {
// if was!! so update and return
return update(dbObj);
}
// now it is save to insert it
return insert();
}
/**
* Called by doUpsert to update the object. It delegates to doUpdate so the
* doUpdate and doInsert method have the same from.
*/
private T update(T dbObj) {
return doUpdate(dbObj);
}
/**
* Called by doUpsert to insert the object. This method starts a new transaction
* and executes the doInsert method in it. The new transaction is rolled back when
* an exception is thrown. The exception is handled in doUpsert
*/
private T insert() {
return doInsert();
}
/**
* Override this method to execute the query that is used to determine if there is an
* existing object in the database
*/
protected abstract T query();
/**
* Override this method to update the object in the database. The object found in the query
* is passed into this method so it can be used to do the updating.
*/
protected abstract T doUpdate(T dbObj);
/**
* Override this method to insert a new object into the database. This method will be called
* when the query method returns null. A DataIntegrityViolationException should be thrown if
* the insert has already occurred. (This is the normal exception thrown when to objects with the
* same id are inserted).
*/
protected abstract T doInsert();
}