/**
 * 
 */
package org.alfresco.repo.transaction;

import java.util.Random;

import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.transaction.TransactionService;
import org.apache.log4j.Logger;
import org.hibernate.StaleObjectStateException;
import org.hibernate.exception.LockAcquisitionException;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DeadlockLoserDataAccessException;

/**
 * A helper that runs a unit of work inside a UserTransaction, 
 * transparently retrying the unit of work if the cause of 
 * failure is an optimistic locking or deadlock condition. 
 * @author britt
 */
public class RetryingTransactionHelper
{
    private static Logger fgLogger = Logger.getLogger(RetryingTransactionHelper.class);
    
    /**
     * Reference to the TransactionService instance.
     */
    private TransactionService fTxnService;
    
    /**
     * The maximum number of retries. -1 for infinity.
     */
    private int fMaxRetries;
    
    /**
     * Random number generator for retry delays.
     */
    private Random fRandom;
    
    /**
     * Callback interface
     * @author britt
     */
    public interface Callback
    {
        public Object execute();
    };

    /**
     * Default constructor.
     */
    public RetryingTransactionHelper()
    {
        fRandom = new Random(System.currentTimeMillis());
    }
    
    // Setters.
    /**
     * Set the TransactionService.
     */
    public void setTransactionService(TransactionService service)
    {
        fTxnService = service;
    }
    
    /**
     * Set the maximimum number of retries. -1 for infinity.
     */
    public void setMaxRetries(int maxRetries)
    {
        fMaxRetries = maxRetries;
    }
    
    /**
     * Execute a callback in a transaction until it succeeds, fails 
     * because of an error not the result of an optimistic locking failure,
     * or a deadlock loser failure, or until a maximum number of retries have
     * been attempted. 
     * @param cb The callback containing the unit of work.
     * @param readOnly Whether this is a read only transaction.
     * @return The result of the unit of work.
     */
    public Object doInTransaction(Callback cb, boolean readOnly)
    {
        // Track the last exception caught, so that we
        // can throw it if we run out of retries.
        RuntimeException lastException = null;
        for (int count = 0; fMaxRetries < 0 || count < fMaxRetries; ++count)
        {
            UserTransaction txn = null;
            boolean isNew = false;
            try
            {
                txn = fTxnService.getUserTransaction(readOnly);
                // Do we need to handle transaction demarcation.  If 
                // no, we cannot do retries, that will be up to the containing
                // transaction.
                isNew = txn.getStatus() == Status.STATUS_NO_TRANSACTION;
                if (isNew)
                {
                    txn.begin();
                }
                // Do the work.
                Object result = cb.execute();
                // Only commit if we 'own' the transaction.
                if (isNew)
                {
                    txn.commit();
                }
                if (fgLogger.isDebugEnabled())
                {
                    if (count != 0)
                    {
                        fgLogger.debug("Transaction succeeded after " + count + " retries");
                    }
                }
                return result;
            }
            catch (Throwable e)
            {
                // Somebody else 'owns' the transaction, so just rethrow.
                if (!isNew)
                {
                    if (e instanceof RuntimeException)
                    {
                        throw (RuntimeException)e;
                    }
                    else
                    {
                        throw new AlfrescoRuntimeException("Unknown Exception.", e);
                    }
                }
                // Rollback if we can.
                if (txn != null)
                {
                    try 
                    {
                        if (txn.getStatus() != Status.STATUS_ROLLEDBACK)
                        {
                            txn.rollback();
                        }
                    } 
                    catch (IllegalStateException e1) 
                    {
                        throw new AlfrescoRuntimeException("Failure during rollback.", e1);
                    } 
                    catch (SecurityException e1) 
                    {
                        throw new AlfrescoRuntimeException("Failure during rollback.", e1);
                    }
                    catch (SystemException e1) 
                    {
                        throw new AlfrescoRuntimeException("Failure during rollback.", e1);
                    }
                }
                lastException = (e instanceof RuntimeException) ? 
                        (RuntimeException)e : new AlfrescoRuntimeException("Unknown Exception in Transaction.", e);
                Throwable t = e;
                boolean shouldRetry = false;
                while (t != null)
                {
                    // These are the 'OK' exceptions. These mean we can retry.
                    if (t instanceof ConcurrencyFailureException ||
                        t instanceof DeadlockLoserDataAccessException ||
                        t instanceof StaleObjectStateException ||
                        t instanceof LockAcquisitionException)
                    {
                        shouldRetry = true;
                        // Sleep a random amount of time before retrying.
                        // The sleep interval increases with the number of retries.
                        try
                        {
                            Thread.sleep(fRandom.nextInt(500 * count + 500));
                        }
                        catch (InterruptedException ie)
                        {
                            // Do nothing.                                                                                  
                        }
                        break;
                    }
                    if (t == t.getCause())
                    {
                        break;
                    }
                    t = t.getCause();
                }
                if (shouldRetry)
                {
                    continue;
                }
                // It was a 'bad' exception.
                if (e instanceof RuntimeException)
                {
                    throw (RuntimeException)e;
                }
                throw new AlfrescoRuntimeException("Exception in Transaction.", e);
            }
        }
        // We've worn out our welcome and retried the maximum number of times.
        // So, fail.
        throw lastException;
    }
}