/* * 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 the following location: * * 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.cas.ticket.registry.support; import java.util.Calendar; import java.util.Date; 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.PersistenceException; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Transactional; /** * JPA 2.0 implementation of an exclusive, non-reentrant lock. * * @author Marvin S. Addison * */ public class JpaLockingStrategy implements LockingStrategy { /** Default lock timeout is 1 hour. */ public static final int DEFAULT_LOCK_TIMEOUT = 3600; /** Transactional entity manager from Spring context. */ @NotNull @PersistenceContext protected EntityManager entityManager; /** Logger instance. */ private final Logger logger = LoggerFactory.getLogger(getClass()); /** * 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. */ @NotNull private String applicationId; /** Unique identifier that identifies the client using this lock instance. */ @NotNull private String uniqueId; /** Amount of time in seconds lock may be held. */ private int lockTimeout = DEFAULT_LOCK_TIMEOUT; /** * @param id 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. */ public void setApplicationId(final String id) { this.applicationId = id; } /** * @param id 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. */ public void setUniqueId(final String id) { this.uniqueId = id; } /** * @param seconds 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 void setLockTimeout(final int seconds) { if (seconds < 0) { throw new IllegalArgumentException("Lock timeout must be non-negative."); } this.lockTimeout = seconds; } /** {@inheritDoc} */ @Override @Transactional(readOnly = false) public boolean acquire() { Lock lock; try { lock = entityManager.find(Lock.class, applicationId, LockModeType.PESSIMISTIC_WRITE); } catch (final PersistenceException e) { logger.debug("{} failed querying for {} lock.", new Object[] {uniqueId, applicationId, e}); return false; } boolean result = false; if (lock != null) { final Date expDate = lock.getExpirationDate(); if (lock.getUniqueId() == null) { // No one currently possesses lock logger.debug("{} trying to acquire {} lock.", uniqueId, applicationId); result = acquire(entityManager, lock); } else if (expDate != null && new Date().after(expDate)) { // Acquire expired lock regardless of who formerly owned it logger.debug("{} trying to acquire expired {} lock.", uniqueId, applicationId); result = acquire(entityManager, lock); } } else { // First acquisition attempt for this applicationId logger.debug("Creating {} lock initially held by {}.", applicationId, uniqueId); result = acquire(entityManager, new Lock()); } return result; } /** {@inheritDoc} */ @Override @Transactional(readOnly = false) public void release() { final Lock lock = entityManager.find(Lock.class, applicationId, LockModeType.PESSIMISTIC_WRITE); if (lock == null) { return; } // Only the current owner can release the lock final String owner = lock.getUniqueId(); if (uniqueId.equals(owner)) { lock.setUniqueId(null); lock.setExpirationDate(null); logger.debug("Releasing {} lock held by {}.", applicationId, uniqueId); entityManager.persist(lock); } else { throw new IllegalStateException("Cannot release lock owned by " + owner); } } /** * Gets the current owner of the lock as determined by querying for * uniqueId. * * @return Current lock owner or null if no one presently owns lock. */ @Transactional(readOnly = true) public String getOwner() { final Lock lock = entityManager.find(Lock.class, applicationId); if (lock != null) { return lock.getUniqueId(); } return null; } /** {@inheritDoc} */ @Override public String toString() { return uniqueId; } private boolean acquire(final EntityManager em, final Lock lock) { lock.setUniqueId(uniqueId); if (lockTimeout > 0) { final Calendar cal = Calendar.getInstance(); cal.add(Calendar.SECOND, lockTimeout); lock.setExpirationDate(cal.getTime()); } else { lock.setExpirationDate(null); } boolean success = false; try { if (lock.getApplicationId() != null) { em.merge(lock); } else { lock.setApplicationId(applicationId); em.persist(lock); } success = true; } catch (final PersistenceException e) { success = false; if (logger.isDebugEnabled()) { logger.debug("{} could not obtain {} lock.", new Object[] {uniqueId, applicationId, e}); } else { logger.info("{} could not obtain {} lock.", uniqueId, applicationId); } } return success; } /** * Describes a database lock. * * @author Marvin S. Addison * */ @Entity @Table(name = "locks") public static class Lock { /** 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. */ @Temporal(TemporalType.TIMESTAMP) @Column(name="expiration_date") private Date expirationDate; /** * @return the applicationId */ public String getApplicationId() { return applicationId; } /** * @param applicationId the applicationId to set */ public void setApplicationId(final String applicationId) { this.applicationId = applicationId; } /** * @return the uniqueId */ public String getUniqueId() { return uniqueId; } /** * @param uniqueId the uniqueId to set */ public void setUniqueId(final String uniqueId) { this.uniqueId = uniqueId; } /** * @return the expirationDate */ public Date getExpirationDate() { return expirationDate; } /** * @param expirationDate the expirationDate to set */ public void setExpirationDate(final Date expirationDate) { this.expirationDate = expirationDate; } } }