/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume */ package org.eclipse.ecr.core.storage.sql; import java.io.Serializable; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.ecr.core.api.Lock; import org.eclipse.ecr.core.storage.StorageException; import org.nuxeo.common.utils.XidImpl; /** * Manager of locks that serializes access to them. * <p> * The public methods called by the session are {@link #setLock}, * {@link #removeLock} and {@link #getLock}. Method {@link #shutdown} must be * called when done with the lock manager. * <p> * In cluster mode, changes are executed in a begin/commit so that tests/updates * can be atomic. * <p> * Transaction management can be done by hand because we're dealing with a * low-level {@link Mapper} and not something wrapped by a JCA pool. */ public class LockManager { private static final Log log = LogFactory.getLog(LockManager.class); /** * The mapper to use. In this mapper we only ever touch the lock table, so * no need to deal with fulltext and complex saves, and we don't do * prefetch. */ protected final Mapper mapper; /** * If clustering is enabled then we have to wrap test/set and test/remove in * a transaction. */ protected final boolean clusteringEnabled; /** * Lock serializing access to the mapper. */ protected final ReentrantLock serializationLock; /** * Counter to avoid having two identical transaction ids. * <p> * Used under {@link #serializationLock}. */ protected static AtomicLong txCounter = new AtomicLong(); protected static final Lock NULL_LOCK = new Lock(null, null); protected final boolean caching; /** * A cache of locks, used only in non-cluster mode, when this lock manager * is the only one dealing with locks. * <p> * Used under {@link #serializationLock}. */ protected final LRUCache<Serializable, Lock> lockCache; protected static final int CACHE_SIZE = 100; protected static class LRUCache<K, V> extends LinkedHashMap<K, V> { private static final long serialVersionUID = 1L; private final int max; public LRUCache(int max) { super(max, 1.0f, true); this.max = max; } @Override protected boolean removeEldestEntry(Entry<K, V> eldest) { return size() > max; } } /** * Creates a lock manager using the given mapper. * <p> * The mapper will from then on be only used and closed by the lock manager. * <p> * {@link #shutdown} must be called when done with the lock manager. */ public LockManager(Mapper mapper, boolean clusteringEnabled) { this.mapper = mapper; this.clusteringEnabled = clusteringEnabled; serializationLock = new ReentrantLock(true); // fair caching = !clusteringEnabled; lockCache = caching ? new LRUCache<Serializable, Lock>(CACHE_SIZE) : null; } /** * Shuts down the lock manager. */ public void shutdown() throws StorageException { serializationLock.lock(); try { mapper.close(); } finally { serializationLock.unlock(); } } /** * Gets the lock on a document. */ public Lock getLock(final Serializable id) throws StorageException { serializationLock.lock(); try { Lock lock; if (caching && (lock = lockCache.get(id)) != null) { return lock == NULL_LOCK ? null : lock; } // no transaction needed, single operation lock = mapper.getLock(id); if (caching) { lockCache.put(id, lock == null ? NULL_LOCK : lock); } return lock; } finally { serializationLock.unlock(); } } /** * Locks a document. */ public Lock setLock(Serializable id, Lock lock) throws StorageException { int RETRIES = 10; for (int i = 0; i < RETRIES; i++) { if (i > 0) { log.debug("Retrying lock on " + id + ": try " + (i + 1)); } try { return setLockInternal(id, lock); } catch (StorageException e) { Throwable c = e.getCause(); if (c != null && c instanceof SQLException && isDuplicateKeyException((SQLException) c)) { // cluster: two simultaneous inserts // retry continue; } throw e; } } throw new StorageException("Failed to lock " + id + ", too much concurrency (tried " + RETRIES + " times)"); } /** * Is the exception about a duplicate primary key? */ protected boolean isDuplicateKeyException(SQLException e) { String sqlState = e.getSQLState(); if ("23000".equals(sqlState)) { // MySQL: Duplicate entry ... for key ... // Oracle: unique constraint ... violated // SQL Server: Violation of PRIMARY KEY constraint return true; } if ("23001".equals(sqlState)) { // H2: Unique index or primary key violation return true; } if ("23505".equals(sqlState)) { // PostgreSQL: duplicate key value violates unique constraint return true; } return false; } protected Lock setLockInternal(final Serializable id, final Lock lock) throws StorageException { serializationLock.lock(); try { Lock oldLock; if (caching && (oldLock = lockCache.get(id)) != null && oldLock != NULL_LOCK) { return oldLock; } oldLock = callInTransaction(new Callable<Lock>() { @Override public Lock call() throws Exception { return mapper.setLock(id, lock); } }); if (caching && oldLock == null) { lockCache.put(id, lock == null ? NULL_LOCK : lock); } return oldLock; } finally { serializationLock.unlock(); } } /** * Unlocks a document. */ public Lock removeLock(final Serializable id, final String owner) throws StorageException { serializationLock.lock(); try { Lock oldLock = null; if (caching && (oldLock = lockCache.get(id)) == NULL_LOCK) { return null; } if (oldLock != null && !canLockBeRemoved(oldLock, owner)) { // existing mismatched lock, flag failure oldLock = new Lock(oldLock, true); } else { if (oldLock == null) { oldLock = callInTransaction(new Callable<Lock>() { @Override public Lock call() throws Exception { return mapper.removeLock(id, owner, false); } }); } else { // we know the previous lock, we can force // no transaction needed, single operation mapper.removeLock(id, owner, true); } } if (caching) { if (oldLock != null && oldLock.getFailed()) { // failed, but we now know the existing lock lockCache.put(id, new Lock(oldLock, false)); } else { lockCache.put(id, NULL_LOCK); } } return oldLock; } finally { serializationLock.unlock(); } } /** * Calls the callable, inside a transaction if in cluster mode. * <p> * Called under {@link #serializationLock}. */ protected Lock callInTransaction(Callable<Lock> callable) throws StorageException { Xid xid = null; boolean txStarted = false; boolean txSuccess = false; try { if (clusteringEnabled) { xid = new XidImpl("nuxeolockmanager." + System.currentTimeMillis() + "." + txCounter.incrementAndGet()); try { mapper.start(xid, XAResource.TMNOFLAGS); } catch (XAException e) { throw new StorageException(e); } txStarted = true; } // else no need to process invalidations, // only this mapper writes locks // actual call Lock result; try { result = callable.call(); } catch (StorageException e) { throw e; } catch (Exception e) { throw new StorageException(e); } txSuccess = true; return result; } finally { if (txStarted) { try { if (txSuccess) { mapper.end(xid, XAResource.TMSUCCESS); mapper.commit(xid, true); } else { mapper.end(xid, XAResource.TMFAIL); mapper.rollback(xid); } } catch (XAException e) { throw new StorageException(e); } } } } public void clearCaches() { serializationLock.lock(); try { if (caching) { lockCache.clear(); } } finally { serializationLock.unlock(); } } /** * Checks if a given lock can be removed by the given owner. * * @param lock the lock * @param owner the owner (may be {@code null}) * @return {@code true} if the lock can be removed */ public static boolean canLockBeRemoved(Lock lock, String owner) { return owner == null || owner.equals(lock.getOwner()); } }