Package org.jasig.portal.concurrency.locking

Source Code of org.jasig.portal.concurrency.locking.JpaClusterLockDao

/**
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you 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 org.jasig.portal.concurrency.locking;

import javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;

import org.hibernate.exception.ConstraintViolationException;
import org.jasig.portal.IPortalInfoProvider;
import org.jasig.portal.jpa.BasePortalJpaDao;
import org.jasig.portal.jpa.cache.EntityManagerCache;
import org.jasig.portal.utils.cache.CacheKey;
import org.joda.time.Duration;
import org.joda.time.ReadableDuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;

/**
* DB based locking DAO using JPA2 locking APIs
*
* @author Eric Dalquist
* @version $Revision$
*/
@Repository
public class JpaClusterLockDao extends BasePortalJpaDao implements IClusterLockDao {
    private static final String CLUSTER_MUTEX_SOURCE = JpaClusterLockDao.class.getName() + "_CLUSTER_MUTEX";
   
    protected final Logger logger = LoggerFactory.getLogger(getClass());
   
    private ReadableDuration abandonedLockAge = Duration.standardSeconds(5);
    private IPortalInfoProvider portalInfoProvider;
    private TransactionTemplate newTransactionTemplate;
    private EntityManagerCache entityManagerCache;
   
    /**
     * Maximum age of the {@link ClusterMutex#getLastUpdate()} field for a locked mutex. A ClusterMutex with an
     * old lastUpdate value will be considered abandoned and be forcibly unlocked. Defaults to 5 seconds.
     * <p/>
     * IMPORTANT: this value must be larger than the maximum possible clock skew across all servers in the cluster.
     */
    @Value("${org.jasig.portal.concurrency.locking.ClusterLockDao.abandonedLockAge:PT60S}")
    public void setAbandonedLockAge(ReadableDuration abandonedLockAge) {
        this.abandonedLockAge = abandonedLockAge;
    }

    @Autowired
    public void setPortalInfoProvider(IPortalInfoProvider portalInfoProvider) {
        this.portalInfoProvider = portalInfoProvider;
    }
   
    @Autowired
    public void setPlatformTransactionManager(@Qualifier(BasePortalJpaDao.PERSISTENCE_UNIT_NAME) PlatformTransactionManager platformTransactionManager) {
        this.newTransactionTemplate = new TransactionTemplate(platformTransactionManager);
        this.newTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.newTransactionTemplate.afterPropertiesSet();
    }

    @Autowired
    public void setEntityManagerCache(EntityManagerCache entityManagerCache) {
        this.entityManagerCache = entityManagerCache;
    }

    @Override
    public ClusterMutex getClusterMutex(final String mutexName) {
        //Do a get first
        ClusterMutex clusterMutex = this.getClusterMutexInternal(mutexName);
        if (clusterMutex != null) {
            logger.trace("Retrieved {}", clusterMutex);
            return clusterMutex;
        }

        //No mutex found, try to create it
        createClusterMutex(mutexName);
       
        //Must have been a concurrent create, do another get
        clusterMutex = this.getClusterMutexInternal(mutexName);
        if (clusterMutex != null) {
            logger.trace("Retrieved {}", clusterMutex);
            return clusterMutex;
        }
       
        throw new IllegalStateException("Failed to find or create ClusterMutex " + mutexName);
    }

    @Override
    public ClusterMutex getLock(final String mutexName) {
        return this.executeIgnoreRollback(new TransactionCallback<ClusterMutex>() {
            @Override
            public ClusterMutex doInTransaction(TransactionStatus status) {
                final EntityManager entityManager = getEntityManager();

                final ClusterMutex clusterMutex = getClusterMutex(mutexName);
               
                //Check if the mutex is already locked
                if (clusterMutex.isLocked()) {
                    //Check if the mutex is abandoned
                    if (isLockAbandoned(clusterMutex)) {
                        //Unlock the abandoned mutex
                        unlockAbandonedLock(mutexName);

                        //Attempt to get the lock again
                        return getLock(mutexName);
                    }
                   
                    //Already locked
                    logger.trace("Mutex {} is already locked: {}", mutexName, clusterMutex);
                    return null;
                }
               
                //Lock the mutex and update the DB
                final String uniqueServerName = portalInfoProvider.getUniqueServerName();
                clusterMutex.lock(uniqueServerName);
                entityManager.persist(clusterMutex);
                try {
                    entityManager.flush();
                    logger.trace("Locked {}", clusterMutex);
                }
                catch (OptimisticLockException e) {
                    logger.trace("Mutex {} was locked by another thread or server", mutexName);
                    return null;
                }
               
                return clusterMutex;
            }
        }, null);
    }

    @Override
    public void updateLock(final String mutexName) {
        this.executeIgnoreRollback(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                final EntityManager entityManager = getEntityManager();

                final ClusterMutex clusterMutex = getClusterMutex(mutexName);
               
                validateLockedMutex(clusterMutex);
               
                clusterMutex.updateLock();
                entityManager.persist(clusterMutex);
                try {
                    entityManager.flush();
                    logger.trace("Updated {}", clusterMutex);
                }
                catch (OptimisticLockException e) {
                    final IllegalMonitorStateException imse = new IllegalMonitorStateException("Failed to update " + mutexName + " due to another thread/server updating the mutex");
                    imse.initCause(e);
                    throw imse;
                }
            }
        });
    }

    @Override
    public void releaseLock(final String mutexName) {
        this.executeIgnoreRollback(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                final EntityManager entityManager = getEntityManager();
               
                final ClusterMutex clusterMutex = getClusterMutex(mutexName);
               
                validateLockedMutex(clusterMutex);
               
                clusterMutex.unlock();
                entityManager.persist(clusterMutex);
                try {
                    entityManager.flush();
                    logger.trace("Released {}", clusterMutex);
                }
                catch (OptimisticLockException e) {
                    final IllegalMonitorStateException imse = new IllegalMonitorStateException("Failed to unlock " + mutexName + " due to another thread/server updating the mutex");
                    imse.initCause(e);
                    throw imse;
                }
            }
        });
    }
   
    /**
     * Retrieves a ClusterMutex in a new TX
     */
    protected ClusterMutex getClusterMutexInternal(final String mutexName) {
        final TransactionOperations transactionOperations = this.getTransactionOperations();
        return transactionOperations.execute(new TransactionCallback<ClusterMutex>() {
            @Override
            public ClusterMutex doInTransaction(TransactionStatus status) {
                final CacheKey key = CacheKey.build(CLUSTER_MUTEX_SOURCE, mutexName);
                ClusterMutex clusterMutex = entityManagerCache.get(PERSISTENCE_UNIT_NAME, key);
                if (clusterMutex != null) {
                    return clusterMutex;
                }
               
                final NaturalIdQuery<ClusterMutex> query = createNaturalIdQuery(ClusterMutex.class);
                query.using(ClusterMutex_.name, mutexName);
                clusterMutex = query.load();
               
                entityManagerCache.put(PERSISTENCE_UNIT_NAME, key, clusterMutex);
               
                return clusterMutex;
            }
        });
    }

    /**
     * Creates a new ClusterMutex with the specified name. Returns the created mutex or null
     * if the mutex already exists.
     */
    protected void createClusterMutex(final String mutexName) {
        this.executeIgnoreRollback(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                final EntityManager entityManager = getEntityManager();
                final ClusterMutex clusterMutex = new ClusterMutex(mutexName);
                entityManager.persist(clusterMutex);
                try {
                    entityManager.flush();
                    logger.trace("Created {}", clusterMutex);
                }
                catch (PersistenceException e) {
                    if (e.getCause() instanceof ConstraintViolationException) {
                        //ignore, another thread beat us to creation
                        logger.debug("Failed to create mutex, it was likely created concurrently by another thread: " + clusterMutex, e);
                        return;
                    }

                    //re-throw exception with unhandled cause
                    throw e;
                }
            }
        });
    }

    /**
     * Validates that the specified mutex is locked by this server, throws IllegalMonitorStateException if either test fails
     */
    protected void validateLockedMutex(ClusterMutex clusterMutex) {
        if (!clusterMutex.isLocked()) {
            throw new IllegalMonitorStateException("Mutex is not currently locked, it cannot be updated: " + clusterMutex);
        }
        final String serverName = this.portalInfoProvider.getUniqueServerName();
        if (!serverName.equals(clusterMutex.getServerId())) {
            throw new IllegalMonitorStateException("Mutex is currently locked by another server: " + clusterMutex + " local serverName: " + serverName);
        }
    }

    /**
     * Unlocks an abandoned mutex
     */
    protected void unlockAbandonedLock(final String mutexName) {
        this.executeIgnoreRollback(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                final EntityManager entityManager = getEntityManager();
                final ClusterMutex clusterMutex = getClusterMutex(mutexName);
               
                if (!isLockAbandoned(clusterMutex)) {
                    //No longer abandoned
                    return;
                }
               
                logger.warn("Unlocking abandoned " + clusterMutex);
                clusterMutex.unlock();
                entityManager.persist(clusterMutex);
                try {
                    entityManager.flush();
                }
                catch (OptimisticLockException e) {
                    logger.trace("Abandoned mutex {} was cleared by another thread or server", mutexName);
                }
            }
        });
    }

    /**
     * Checks if the specified mutex is abandoned
     */
    protected boolean isLockAbandoned(final ClusterMutex clusterMutex) {
        return clusterMutex.getLastUpdate() < (System.currentTimeMillis() - abandonedLockAge.getMillis());
    }
   
   
    protected <T> T executeIgnoreRollback(TransactionCallback<T> action) {
        return this.executeIgnoreRollback(action, null);
    }
    /**
     * Utility for using TransactionTemplate when we know that a rollback might happen and just want to ignore it
     */
    protected <T> T executeIgnoreRollback(TransactionCallback<T> action, T rollbackValue) {
        try {
            //Try to create the mutex in a new TX
            return this.newTransactionTemplate.execute(action);
        }
        catch (TransactionSystemException e) {
            if (e.getCause() instanceof RollbackException) {
                //Ignore rollbacks
                return rollbackValue;
            }
           
            //re-throw exception with unhandled cause
            throw e;
        }
    }
}
TOP

Related Classes of org.jasig.portal.concurrency.locking.JpaClusterLockDao

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.