/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.concurrency.locking;
import javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import org.apereo.portal.IPortalInfoProvider;
import org.apereo.portal.jpa.BasePortalJpaDao;
import org.apereo.portal.jpa.cache.EntityManagerCache;
import org.apereo.portal.utils.cache.CacheKey;
import org.hibernate.exception.ConstraintViolationException;
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
*
*/
@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.apereo.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;
}
}
}