package org.apereo.cas.ticket.registry.support; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Transactional; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.Id; import javax.persistence.LockModeType; import javax.persistence.PersistenceContext; import javax.persistence.Table; import javax.persistence.Version; import java.io.Serializable; import java.time.ZoneOffset; import java.time.ZonedDateTime; /** * JPA 2.0 implementation of an exclusive, non-reentrant lock. * * @author Marvin S. Addison * @since 3.0.0 */ @Transactional(transactionManager = "ticketTransactionManager") public class JpaLockingStrategy implements LockingStrategy { private static final Logger LOGGER = LoggerFactory.getLogger(JpaLockingStrategy.class); /** Transactional entity manager from Spring context. */ @PersistenceContext(unitName = "ticketEntityManagerFactory") protected EntityManager entityManager; /** * Application identifier that identifies rows in the locking table, * each one of which may be for a different application or usage within * a single application. */ private String applicationId; /** Unique identifier that identifies the client using this lock instance. */ private String uniqueId; /** Amount of time in seconds lock may be held. */ private long lockTimeout; /** * * @param applicationId Application identifier that identifies a row in the lock * table for which multiple clients vie to hold the lock. * This must be the same for all clients contending for a * particular lock. * @param uniqueId Identifier used to identify this instance in a row of the * lock table. Must be unique across all clients vying for * locks for a given application ID. * @param lockTimeout Maximum amount of time in seconds lock may be held. * A value of zero indicates that locks are held indefinitely. * Use of a reasonable timeout facilitates recovery from node failures, * so setting to zero is discouraged. */ public JpaLockingStrategy(final String applicationId, final String uniqueId, final long lockTimeout) { this.applicationId = applicationId; this.uniqueId = uniqueId; if (lockTimeout < 0) { throw new IllegalArgumentException("Lock timeout must be non-negative."); } this.lockTimeout = lockTimeout; } @Override public boolean acquire() { final Lock lock; try { lock = this.entityManager.find(Lock.class, this.applicationId, LockModeType.OPTIMISTIC); } catch (final Exception e) { LOGGER.debug("[{}] failed querying for [{}] lock.", this.uniqueId, this.applicationId, e); return false; } boolean result = false; if (lock != null) { final ZonedDateTime expDate = lock.getExpirationDate(); if (lock.getUniqueId() == null) { // No one currently possesses lock LOGGER.debug("[{}] trying to acquire [{}] lock.", this.uniqueId, this.applicationId); result = acquire(lock); } else if (expDate == null || ZonedDateTime.now(ZoneOffset.UTC).isAfter(expDate)) { // Acquire expired lock regardless of who formerly owned it LOGGER.debug("[{}] trying to acquire expired [{}] lock.", this.uniqueId, this.applicationId); result = acquire(lock); } } else { // First acquisition attempt for this applicationId LOGGER.debug("Creating [{}] lock initially held by [{}].", applicationId, uniqueId); result = acquire(new Lock()); } return result; } @Override public void release() { final Lock lock = this.entityManager.find(Lock.class, this.applicationId, LockModeType.OPTIMISTIC); if (lock == null) { return; } // Only the current owner can release the lock final String owner = lock.getUniqueId(); if (!this.uniqueId.equals(owner)) { throw new IllegalStateException("Cannot release lock owned by " + owner); } lock.setUniqueId(null); lock.setExpirationDate(null); LOGGER.debug("Releasing [{}] lock held by [{}].", this.applicationId, this.uniqueId); this.entityManager.persist(lock); } @Override public String toString() { return this.uniqueId; } /** * Acquire the lock object. * * @param lock the lock * @return true, if successful */ public boolean acquire(final Lock lock) { lock.setUniqueId(this.uniqueId); if (this.lockTimeout > 0) { lock.setExpirationDate(ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(this.lockTimeout)); } else { lock.setExpirationDate(null); } boolean success; try { if (lock.getApplicationId() != null) { this.entityManager.merge(lock); } else { lock.setApplicationId(this.applicationId); this.entityManager.persist(lock); } success = true; } catch (final Exception e) { success = false; if (LOGGER.isDebugEnabled()) { LOGGER.debug("[{}] could not obtain [{}] lock.", this.uniqueId, this.applicationId, e); } else { LOGGER.info("[{}] could not obtain [{}] lock.", this.uniqueId, this.applicationId); } } return success; } /** * Describes a database lock. * * @author Marvin S. Addison * */ @Entity @Table(name = "locks") private static class Lock implements Serializable { private static final long serialVersionUID = -5750740484289616656L; /** column name that holds application identifier. */ @Id @Column(name="application_id") private String applicationId; /** Database column name that holds unique identifier. */ @Column(name="unique_id") private String uniqueId; /** Database column name that holds expiration date. */ @Column(name="expiration_date") private ZonedDateTime expirationDate; @Version @Column(name = "lockVer", columnDefinition = "integer DEFAULT 0", nullable = false) private Long version = 0L; /** * @return the applicationId */ public String getApplicationId() { return this.applicationId; } /** * @param applicationId the applicationId to set */ public void setApplicationId(final String applicationId) { this.applicationId = applicationId; } /** * @return the uniqueId */ public String getUniqueId() { return this.uniqueId; } /** * @param uniqueId the uniqueId to set */ public void setUniqueId(final String uniqueId) { this.uniqueId = uniqueId; } /** * @return the expirationDate */ public ZonedDateTime getExpirationDate() { return this.expirationDate == null ? null : ZonedDateTime.from(this.expirationDate); } /** * @param expirationDate the expirationDate to set */ public void setExpirationDate(final ZonedDateTime expirationDate) { this.expirationDate = expirationDate; } } }