diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index 8bf5a0e273..7559bc04e3 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -73,6 +73,7 @@ classpath:alfresco/dbscripts/create/2.2/${db.script.dialect}/AlfrescoPostCreate-2.2-Extra.sql classpath:alfresco/dbscripts/create/2.2/${db.script.dialect}/post-create-indexes-04.sql classpath:alfresco/dbscripts/create/3.0/${db.script.dialect}/create-activities-extras.sql + classpath:alfresco/dbscripts/create/3.2/${db.script.dialect}/AlfrescoPostCreate-3.2-LockTables.sql @@ -90,6 +91,7 @@ + diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-LockTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-LockTables.sql new file mode 100644 index 0000000000..c75402b43e --- /dev/null +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.DerbyDialect/AlfrescoPostCreate-3.2-LockTables.sql @@ -0,0 +1,46 @@ +-- +-- Title: Create lock tables +-- Database: Derby +-- Since: V3.2 Schema 2011 +-- Author: Derek Hulley +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_lock_resource +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + qname_ns_id BIGINT NOT NULL, + qname_localname VARCHAR(255) NOT NULL, + CONSTRAINT fk_alf_lockr_ns FOREIGN KEY (qname_ns_id) REFERENCES alf_namespace (id), + CONSTRAINT fk_alf_lockr_id PRIMARY KEY (id), + CONSTRAINT idx_alf_lockr_key UNIQUE (qname_ns_id, qname_localname) +); + +CREATE TABLE alf_lock +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + version BIGINT NOT NULL, + shared_resource_id BIGINT NOT NULL, + excl_resource_id BIGINT NOT NULL, + lock_token VARCHAR(36) NOT NULL, + start_time BIGINT NOT NULL, + expiry_time BIGINT NOT NULL, + CONSTRAINT fk_alf_lock_shared FOREIGN KEY (shared_resource_id) REFERENCES alf_lock_resource (id), + CONSTRAINT fk_alf_lock_excl FOREIGN KEY (excl_resource_id) REFERENCES alf_lock_resource (id), + CONSTRAINT fk_alf_lock_id PRIMARY KEY (id), + CONSTRAINT idx_alf_lock_key UNIQUE (shared_resource_id, excl_resource_id) +); + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V3.2-LockTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V3.2-LockTables', 'Manually executed script upgrade V3.2: Lock Tables', + 0, 2010, -1, 2011, null, 'UNKOWN', 1, 1, 'Script completed' + ); \ No newline at end of file diff --git a/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-LockTables.sql b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-LockTables.sql new file mode 100644 index 0000000000..7e79e02eb3 --- /dev/null +++ b/config/alfresco/dbscripts/create/3.2/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoPostCreate-3.2-LockTables.sql @@ -0,0 +1,46 @@ +-- +-- Title: Create lock tables +-- Database: MySQL InnoDB +-- Since: V3.2 Schema 2011 +-- Author: Derek Hulley +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_lock_resource +( + id BIGINT NOT NULL AUTO_INCREMENT, + version BIGINT NOT NULL, + qname_ns_id BIGINT NOT NULL, + qname_localname VARCHAR(255) NOT NULL, + CONSTRAINT fk_alf_lockr_ns FOREIGN KEY (qname_ns_id) REFERENCES alf_namespace (id), + PRIMARY KEY (id), + UNIQUE INDEX idx_alf_lockr_key (qname_ns_id, qname_localname) +) TYPE=InnoDB; + +CREATE TABLE alf_lock +( + id BIGINT NOT NULL auto_increment, + version BIGINT NOT NULL, + shared_resource_id BIGINT NOT NULL, + excl_resource_id BIGINT NOT NULL, + lock_token VARCHAR(36) NOT NULL, + start_time BIGINT NOT NULL, + expiry_time BIGINT NOT NULL, + CONSTRAINT fk_alf_lock_shared FOREIGN KEY (shared_resource_id) REFERENCES alf_lock_resource (id), + CONSTRAINT fk_alf_lock_excl FOREIGN KEY fk_alf_lock_excl (excl_resource_id) REFERENCES alf_lock_resource (id), + PRIMARY KEY (id), + UNIQUE INDEX idx_alf_lock_key (shared_resource_id, excl_resource_id) +) TYPE=InnoDB; + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V3.2-LockTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V3.2-LockTables', 'Manually executed script upgrade V3.2: Lock Tables', + 0, 2010, -1, 2011, null, 'UNKOWN', 1, 1, 'Script completed' + ); \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/locks-insert-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/locks-insert-SqlMap.xml new file mode 100644 index 0000000000..8725231df6 --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.DerbyDialect/locks-insert-SqlMap.xml @@ -0,0 +1,23 @@ + + + + + + + + + + values IDENTITY_VAL_LOCAL() + + + + + + + values IDENTITY_VAL_LOCAL() + + + + \ No newline at end of file diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/locks-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/locks-common-SqlMap.xml index 33401192c2..d2564bdbc7 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/locks-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/locks-common-SqlMap.xml @@ -19,17 +19,32 @@ + + - + + + + + + + + + + + + + + @@ -40,15 +55,15 @@ - insert into alf_lock (version, shared_resource_id, excl_resource_id, lock_holder) - values (#version#, #sharedResourceId#, #exclusiveResourceId#, lower(#lockHolder#)) + insert into alf_lock (version, shared_resource_id, excl_resource_id, lock_token, start_time, expiry_time) + values (#version#, #sharedResourceId#, #exclusiveResourceId#, lower(#lockToken#), #startTime#, #expiryTime#) - + + + + + + + + + + + + update + alf_lock + set + version = #version#, + lock_token = lower(#lockToken#), + start_time = #startTime#, + expiry_time = #expiryTime# + where + id = #id# and + version = (#version# -1) + + + + + update + alf_lock + set + version = version + 1, + lock_token = lower(?), + expiry_time = ? + where + excl_resource_id = ? and + lock_token = ? + \ No newline at end of file diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index 9d303299c5..0c82160df5 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -1826,4 +1826,16 @@ + + patch.db-V3.2-LockTables + patch.schemaUpgradeScript.description + 0 + 2010 + 2011 + + + classpath:alfresco/dbscripts/create/3.2/${db.script.dialect}/AlfrescoPostCreate-3.2-LockTables.sql + + + diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties index 775d77b00c..fa96338a16 100644 --- a/config/alfresco/version.properties +++ b/config/alfresco/version.properties @@ -19,4 +19,4 @@ version.build=@build-number@ # Schema number -version.schema=2010 +version.schema=2011 diff --git a/source/java/org/alfresco/repo/domain/hibernate/HibernateQNameDAOImpl.java b/source/java/org/alfresco/repo/domain/hibernate/HibernateQNameDAOImpl.java index 026413c9fd..d5f7df9a95 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/HibernateQNameDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/HibernateQNameDAOImpl.java @@ -169,7 +169,9 @@ public class HibernateQNameDAOImpl extends HibernateDaoSupport implements QNameD NamespaceEntity namespace = new NamespaceEntityImpl(); namespace.setUri(namespaceUri); // Persist - Long id = (Long) getSession().save(namespace); + Session session = getSession(); + Long id = (Long) session.save(namespace); + DirtySessionMethodInterceptor.flushSession(session, true); // Cache it namespaceEntityCache.put(id, namespaceUri); namespaceEntityCache.put(namespaceUri, id); diff --git a/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java b/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java index c89b9051d1..19ad130ea3 100644 --- a/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/locks/AbstractLockDAOImpl.java @@ -43,6 +43,8 @@ import org.springframework.dao.ConcurrencyFailureException; */ public abstract class AbstractLockDAOImpl implements LockDAO { + private static final String LOCK_TOKEN_RELEASED = "not-locked"; + private QNameDAO qnameDAO; /** @@ -61,7 +63,7 @@ public abstract class AbstractLockDAOImpl implements LockDAO this.qnameDAO = qnameDAO; } - public boolean getLock(QName lockQName, String lockApplicant, long timeToLive) + public boolean getLock(QName lockQName, String lockToken, long timeToLive) { String qnameNamespaceUri = lockQName.getNamespaceURI(); String qnameLocalName = lockQName.getLocalName(); @@ -71,8 +73,8 @@ public abstract class AbstractLockDAOImpl implements LockDAO lockQName = QName.createQName(qnameNamespaceUri, qnameLocalName.toLowerCase()); qnameLocalName = lockQName.getLocalName(); } - // Force the lock applicant to lowercase - lockApplicant = lockApplicant.toLowerCase(); + // Force the lock token to lowercase + lockToken = lockToken.toLowerCase(); // Resolve the namespace Long qnameNamespaceId = qnameDAO.getOrCreateNamespace(qnameNamespaceUri).getFirst(); @@ -94,7 +96,6 @@ public abstract class AbstractLockDAOImpl implements LockDAO { String localname = lockQNameIter.getLocalName(); // Get the basic lock resource, forcing a create - // TODO: Pull back all lock resources in a single query LockResourceEntity lockResource = getLockResource(qnameNamespaceId, localname); if (lockResource == null) { @@ -105,12 +106,12 @@ public abstract class AbstractLockDAOImpl implements LockDAO } // Now, get all locks for the resources we will need - List existingLocks = getLocks(requiredLockResourceIds); + List existingLocks = getLocksBySharedResourceIds(requiredLockResourceIds); Map existingLocksMap = new HashMap(); // Check them and make sure they don't prevent locks for (LockEntity existingLock : existingLocks) { - boolean canTakeLock = canTakeLock(existingLock, lockApplicant, requiredExclusiveLockResourceId); + boolean canTakeLock = canTakeLock(existingLock, lockToken, requiredExclusiveLockResourceId); if (!canTakeLock) { return false; @@ -129,7 +130,7 @@ public abstract class AbstractLockDAOImpl implements LockDAO { requiredLock = existingLocksMap.get(requiredLock); // Do an update - throw new UnsupportedOperationException(); + updateLock(requiredLock, lockToken, timeToLive); } else { @@ -137,18 +138,78 @@ public abstract class AbstractLockDAOImpl implements LockDAO requiredLock = createLock( requiredLockResourceId, requiredExclusiveLockResourceId, - lockApplicant, + lockToken, timeToLive); } } return true; } - private boolean canTakeLock(LockEntity existingLock, String lockApplicant, Long desiredExclusiveLock) + public boolean refreshLock(QName lockQName, String lockToken, long timeToLive) { - if (EqualsHelper.nullSafeEquals(existingLock.getLockHolder(), lockApplicant)) + return updateLocks(lockQName, lockToken, lockToken, timeToLive); + } + + public boolean releaseLock(QName lockQName, String lockToken) + { + return updateLocks(lockQName, lockToken, LOCK_TOKEN_RELEASED, 0L); + } + + /** + * Put new values against the given exclusive lock. This works against the related locks as + * well. + */ + private boolean updateLocks(QName lockQName, String lockToken, String newLockToken, long timeToLive) + { + String qnameNamespaceUri = lockQName.getNamespaceURI(); + String qnameLocalName = lockQName.getLocalName(); + // Force lower case for case insensitivity + if (!qnameLocalName.toLowerCase().equals(qnameLocalName)) { - // The lock applicant to be is also the current lock holder. + lockQName = QName.createQName(qnameNamespaceUri, qnameLocalName.toLowerCase()); + qnameLocalName = lockQName.getLocalName(); + } + // Force the lock token to lowercase + lockToken = lockToken.toLowerCase(); + + // Resolve the namespace + Long qnameNamespaceId = qnameDAO.getOrCreateNamespace(qnameNamespaceUri).getFirst(); + + // Get the lock resource for the exclusive lock. + // All the locks that are created will need the exclusive case. + LockResourceEntity exclusiveLockResource = getLockResource(qnameNamespaceId, qnameLocalName); + if (exclusiveLockResource == null) + { + // If the exclusive lock doesn't exist, the locks don't exist + return false; + } + Long exclusiveLockResourceId = exclusiveLockResource.getId(); + // Split the lock name + List lockQNames = splitLockQName(lockQName); + // We just need to know how many resources needed updating. + // They will all share the same exclusive lock resource + int requiredUpdateCount = lockQNames.size(); + // Update + int updateCount = updateLocks(exclusiveLockResourceId, lockToken, newLockToken, timeToLive); + // Check + if (updateCount != requiredUpdateCount) + { + return false; + } + else + { + return true; + } + } + + /** + * Validate if a lock can be taken or not. + */ + private boolean canTakeLock(LockEntity existingLock, String lockToken, Long desiredExclusiveLock) + { + if (EqualsHelper.nullSafeEquals(existingLock.getLockToken(), lockToken)) + { + // The lock token is the same. // Regardless of lock expiry, the lock can be taken return true; } @@ -159,7 +220,7 @@ public abstract class AbstractLockDAOImpl implements LockDAO } else if (existingLock.isExclusive()) { - // It's a valid, exclusive lock held by someone else ... + // It's a valid, exclusive lock held using a different token ... return false; } else if (desiredExclusiveLock.equals(existingLock.getSharedResourceId())) @@ -173,48 +234,86 @@ public abstract class AbstractLockDAOImpl implements LockDAO return true; } } - + /** * Override to get the unique, lock resource entity if one exists. * - * @param qnameNamespaceId the namespace entity ID - * @param qnameLocalName the lock localname - * @return Returns the lock resource entity, - * or null if it doesn't exist + * @param qnameNamespaceId the namespace entity ID + * @param qnameLocalName the lock localname + * @return Returns the lock resource entity, + * or null if it doesn't exist */ protected abstract LockResourceEntity getLockResource(Long qnameNamespaceId, String qnameLocalName); /** * Create a unique lock resource * - * @param qnameNamespaceId the namespace entity ID - * @param qnameLocalName the lock localname - * @return Returns the newly created lock resource entity + * @param qnameNamespaceId the namespace entity ID + * @param qnameLocalName the lock localname + * @return Returns the newly created lock resource entity */ protected abstract LockResourceEntity createLockResource(Long qnameNamespaceId, String qnameLocalName); /** - * Get any existing lock data for the resources required. The locks returned are not filtered and + * @param id the lock instance ID + * @return Returns the lock, if it exists, otherwise null + */ + protected abstract LockEntity getLock(Long id); + + /** + * @param sharedResourceId the shared lock resource ID + * @param exclusiveResourceId the exclusive lock resource ID + * @return Returns the lock, if it exists, otherwise null + */ + protected abstract LockEntity getLock(Long sharedResourceId, Long exclusiveResourceId); + + /** + * Get any existing lock data for the shared resources. The locks returned are not filtered and * may be expired. * - * @param lockResourceIds a list of resource IDs for which to retrieve the current locks + * @param lockResourceIds a list of shared resource IDs for which to retrieve the current locks * @return Returns a list of locks (expired or not) for the given lock resources */ - protected abstract List getLocks(List lockResourceIds); + protected abstract List getLocksBySharedResourceIds(List sharedLockResourceIds); /** * Create a new lock. - * @param sharedResourceId the specific resource to lock - * @param exclusiveResourceId the exclusive lock that is being sought - * @param lockApplicant the ID of the lock applicant - * @param timeToLive the time, in milliseconds, for the lock to remain valid - * @return Returns the new lock + * @param sharedResourceId the specific resource to lock + * @param exclusiveResourceId the exclusive lock that is being sought + * @param lockToken the lock token to assign + * @param timeToLive the time, in milliseconds, for the lock to remain valid + * @return Returns the new lock * @throws ConcurrencyFailureException if the lock was already taken at the time of creation */ protected abstract LockEntity createLock( Long sharedResourceId, Long exclusiveResourceId, - String lockApplicant, + String lockToken, + long timeToLive); + + /** + * Update an existing lock + * @param lockEntity the specific lock to update + * @param lockApplicant the new lock token + * @param timeToLive the new lock time, in milliseconds, for the lock to remain valid + * @return Returns the updated lock + */ + protected abstract LockEntity updateLock( + LockEntity lockEntity, + String lockToken, + long timeToLive); + + /** + * @param exclusiveLockResourceId the exclusive resource ID being locks + * @param oldLockToken the lock token to change from + * @param newLockToken the new lock token + * @param timeToLive the new time to live (in milliseconds) + * @return the number of rows updated + */ + protected abstract int updateLocks( + Long exclusiveLockResourceId, + String oldLockToken, + String newLockToken, long timeToLive); /** @@ -222,8 +321,8 @@ public abstract class AbstractLockDAOImpl implements LockDAO * separator on the localname. The namespace is preserved. The provided qualified * name will always be the last component in the returned list. * - * @param lockQName the lock name to split into it's higher-level paths - * @return Returns the namespace ID along with the ordered localnames + * @param lockQName the lock name to split into it's higher-level paths + * @return Returns the namespace ID along with the ordered localnames */ protected List splitLockQName(QName lockQName) { diff --git a/source/java/org/alfresco/repo/domain/locks/LockDAO.java b/source/java/org/alfresco/repo/domain/locks/LockDAO.java index 0b9ba2222e..990e6490fb 100644 --- a/source/java/org/alfresco/repo/domain/locks/LockDAO.java +++ b/source/java/org/alfresco/repo/domain/locks/LockDAO.java @@ -37,12 +37,43 @@ public interface LockDAO /** * Aquire a given exclusive lock, assigning it (and any implicitly shared locks) a * timeout. All shared locks are implicitly taken as well. + *

+ * A lock can be re-taken if it has expired and if the lock token has not changed * * @param lockQName the unique name of the lock to acquire - * @param lockApplicant the potential lock holder's identifier (max 36 chars) + * @param lockToken the potential lock token (max 36 chars) * @param timeToLive the time (in milliseconds) that the lock must remain * @return Returns true if the lock was taken, * otherwise false */ - boolean getLock(QName lockQName, String lockApplicant, long timeToLive); + boolean getLock(QName lockQName, String lockToken, long timeToLive); + + /** + * Refresh a held lock. This is successful if the lock in question still exists + * and if the lock token has not changed. Lock expiry does not prevent the lock + * from being refreshed. + * + * @param lockQName the unique name of the lock to update + * @param lockToken the lock token for the lock held + * @param timeToLive the new time to live (in milliseconds) + * @return Returns true if the lock was updated, + * otherwise false + */ + boolean refreshLock(QName lockQName, String lockToken, long timeToLive); + + /** + * Release a lock. The lock token must still apply and all the shared and exclusive + * locks need to still be present. Lock expiration does not prevent this operation + * from succeeding. + *

+ * Note: Failure to release a lock due to a exception condition is dealt with by + * passing the exception out. + * + * @param lockQName the unique name of the lock to release + * @param lockToken the current lock token + * @return Returns true if all the required locks were + * (still) held under the lock token and were + * valid at the time of release, otherwise false + */ + boolean releaseLock(QName lockQName, String lockToken); } diff --git a/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java new file mode 100644 index 0000000000..b5b740bb43 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/locks/LockDAOTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.domain.locks; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import junit.framework.TestCase; + +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @see LockDAO + * + * @author Derek Hulley + * @since 3.2 + */ +public class LockDAOTest extends TestCase +{ + public static final String NAMESPACE = "http://www.alfresco.org/test/LockDAOTest"; + + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private LockDAO lockDAO; + // Lock names for the tests + private QName lockA; + private QName lockAA; + private QName lockAAA; + private QName lockAAB; + private QName lockAAC; + private QName lockAB; + private QName lockABA; + private QName lockABB; + private QName lockABC; + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + txnHelper.setMinRetryWaitMs(10); + txnHelper.setRetryWaitIncrementMs(10); + txnHelper.setMaxRetryWaitMs(50); + + lockDAO = (LockDAO) ctx.getBean("lockDAO"); + // Get the test name + String testName = getName(); + // Build lock names for the test + lockA = QName.createQName(NAMESPACE, "a-" + testName); + lockAA = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName); + lockAAA = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".a-" + testName); + lockAAB = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".b-" + testName); + lockAAC = QName.createQName(NAMESPACE, "a-" + testName + ".a-" + testName + ".c-" + testName); + lockAB = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName); + lockABA = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".a-" + testName); + lockABB = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".b-" + testName); + lockABC = QName.createQName(NAMESPACE, "a-" + testName + ".b-" + testName + ".c-" + testName); + } + + private String lock(final QName lockName, final long timeToLive, boolean expectSuccess) + { + String token = lock(lockName, timeToLive); + if (expectSuccess) + { + assertNotNull( + "Expected to get lock " + lockName + " with TTL of " + timeToLive, + token); + } + else + { + assertNull( + "Expected lock " + lockName + " to have been denied", + token); + } + return token; + } + /** + * Do the lock in a new transaction + * @return Returns the lock token or null if it didn't work + */ + private String lock(final QName lockName, final long timeToLive) + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + String txnId = AlfrescoTransactionSupport.getTransactionId(); + boolean locked = lockDAO.getLock(lockName, txnId, timeToLive); + return locked ? txnId : null; + } + }; + return txnHelper.doInTransaction(callback); + } + + private void refresh(final QName lockName, final String lockToken, final long timeToLive, boolean expectSuccess) + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Boolean execute() throws Throwable + { + return lockDAO.refreshLock(lockName, lockToken, timeToLive); + } + }; + Boolean released = txnHelper.doInTransaction(callback); + if (expectSuccess) + { + assertTrue( + "Expected to have refreshed lock " + lockName, + released.booleanValue()); + } + else + { + assertFalse( + "Expected to have failed to refresh lock " + lockName, + released.booleanValue()); + } + } + + private void release(final QName lockName, final String lockToken, boolean expectSuccess) + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Boolean execute() throws Throwable + { + return lockDAO.releaseLock(lockName, lockToken); + } + }; + Boolean released = txnHelper.doInTransaction(callback); + if (expectSuccess) + { + assertTrue( + "Expected to have released lock " + lockName, + released.booleanValue()); + } + else + { + assertFalse( + "Expected to have failed to release lock " + lockName, + released.booleanValue()); + } + } + + public void testGetLockBasic() throws Exception + { + lock(lockAAA, 500L, true); + } + + /** + * Ensure that the lock tables and queries scale + */ + public void testLockTableScaling() throws Exception + { + int count = 500; + long before = System.currentTimeMillis(); + for (int i = 1; i <= count; i++) + { + QName lockName = QName.createQName(lockAAA.getNamespaceURI(), lockAAA.getLocalName() + "-" + i); + lock(lockName, 500L, true); + if (i % 100 == 0) + { + long after = System.currentTimeMillis(); + System.out.println("Creation of " + i + " locks took " + (after-before)/1000 + "s"); + } + } + } + + public void testGetLockFailureBasic() throws Exception + { + lock(lockAAA, 500L, true); + lock(lockAAA, 0L, false); + } + + public void testSharedLocks() throws Exception + { + lock(lockAAA, 500L, true); + lock(lockAAB, 500L, true); + lock(lockAAC, 500L, true); + lock(lockABA, 500L, true); + lock(lockABB, 500L, true); + lock(lockABC, 500L, true); + } + + public void testExclusiveLockBlockedByShared() throws Exception + { + lock(lockAAA, 100L, true); + lock(lockAA, 100L, false); + lock(lockAB, 100L, true); + lock(lockA, 100L, false); + lock(lockABA, 100L, false); + } + + public void testReleaseLockBasic() throws Exception + { + String token = lock(lockAAA, 500000L, true); + release(lockAAA, token, true); + token = lock(lockAAA, 0L, true); + } + + public void testSharedLockAndRelease() throws Exception + { + String tokenAAA = lock(lockAAA, 5000L, true); + String tokenAAB = lock(lockAAB, 5000L, true); + String tokenAAC = lock(lockAAC, 5000L, true); + String tokenABA = lock(lockABA, 5000L, true); + String tokenABB = lock(lockABB, 5000L, true); + String tokenABC = lock(lockABC, 5000L, true); + // Can't lock shared resources + lock(lockAA, 0L, false); + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockAAA, tokenAAA, true); + lock(lockAA, 0L, false); + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockAAB, tokenAAB, true); + lock(lockAA, 0L, false); + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockAAC, tokenAAC, true); + String tokenAA = lock(lockAA, 5000L, true); // This should be open now + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockABA, tokenABA, true); + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockABB, tokenABB, true); + lock(lockAB, 0L, false); + lock(lockA, 0L, false); + // Release a lock and check again + release(lockABC, tokenABC, true); + String tokenAB = lock(lockAB, 5000L, true); + lock(lockA, 0L, false); + // Release AA and AB + release(lockAA, tokenAA, true); + release(lockAB, tokenAB, true); + String tokenA = lock(lockA, 5000L, true); + // ... and release + release(lockA, tokenA, true); + } + + public synchronized void testLockExpiry() throws Exception + { + lock(lockAAA, 50L, true); + this.wait(50L); + lock(lockAA, 50L, true); + this.wait(50L); + lock(lockA, 100L, true); + } + + /** + * Check that locks grabbed away due to expiry cannot be released + * @throws Exception + */ + public synchronized void testLockExpiryAndRelease() throws Exception + { + String tokenAAA = lock(lockAAA, 500L, true); + release(lockAAA, tokenAAA, true); + tokenAAA = lock(lockAAA, 50L, true); // Make sure we can re-acquire the lock + this.wait(50L); // Wait for expiry + String grabbedTokenAAAA = lock(lockAAA, 50L, true); // Grabbed lock over the expiry + release(lockAAA, tokenAAA, false); // Can't release any more + this.wait(50L); // Wait for expiry + release(lockAAA, grabbedTokenAAAA, true); // Proof that expiry, on it's own, doesn't prevent release + } + + public synchronized void testLockRefresh() throws Exception + { + String tokenAAA = lock(lockAAA, 1000L, true); + // Loop, refreshing and testing + for (int i = 0; i < 40; i++) + { + wait(50L); + // It will have expired, but refresh it anyway + refresh(lockAAA, tokenAAA, 1000L, true); + // Check that it is still holding + lock(lockAAA, 0L, false); + } + } + + /** + * Uses a thread lock to ensure that the lock DAO only allows locks through one at a time. + */ + public synchronized void testConcurrentLockAquisition() throws Exception + { + ReentrantLock threadLock = new ReentrantLock(); + GetLockThread[] threads = new GetLockThread[50]; + for (int i = 0; i < threads.length; i++) + { + threads[i] = new GetLockThread(threadLock); + threads[i].start(); + } + // Wait a bit and see if any encountered errors + boolean allDone = false; + waitLoop: + for (int waitLoop = 0; waitLoop < 50; waitLoop++) + { + wait(2000L); + for (int i = 0; i < threads.length; i++) + { + if (!threads[i].done) + { + continue waitLoop; + } + } + // All the threads are done + allDone = true; + break; + } + // Check that all the threads got a turn + if (!allDone) + { + fail("Not all threads managed to acquire the lock"); + } + // Get errors + StringBuilder errors = new StringBuilder(512); + for (int i = 0; i < threads.length; i++) + { + if (threads[i].error != null) + { + errors.append("\nThread ").append(i).append(" error: ").append(threads[i].error); + } + } + if (errors.toString().length() > 0) + { + fail(errors.toString()); + } + } + + /** + * Checks that the lock via the DAO forces a serialization + */ + private class GetLockThread extends Thread + { + private final ReentrantLock threadLock; + private boolean done; + private String error; + private GetLockThread(ReentrantLock threadLock) + { + this.threadLock = threadLock; + this.done = false; + this.error = null; + setDaemon(true); + } + @Override + public synchronized void run() + { + boolean gotLock = false; + try + { + String tokenAAA = null; + while (true) + { + tokenAAA = lock(lockAAA, 100000L); // Lock for a long time + if (tokenAAA != null) + { + break; // Got the lock + } + try { wait(20L); } catch (InterruptedException e) {} + } + gotLock = threadLock.tryLock(0, TimeUnit.MILLISECONDS); + if (!gotLock) + { + error = "Got lock via DAO but not via thread lock"; + return; + } + release(lockAAA, tokenAAA, true); + } + catch (Throwable e) + { + error = e.getMessage(); + } + finally + { + done = true; + if (gotLock) + { + threadLock.unlock(); + } + } + } + } +} diff --git a/source/java/org/alfresco/repo/domain/locks/LockEntity.java b/source/java/org/alfresco/repo/domain/locks/LockEntity.java index bc325b0ac5..2353e285f5 100644 --- a/source/java/org/alfresco/repo/domain/locks/LockEntity.java +++ b/source/java/org/alfresco/repo/domain/locks/LockEntity.java @@ -41,9 +41,9 @@ public class LockEntity private Long version; private Long sharedResourceId; private Long exclusiveResourceId; - private String lockHolder; + private String lockToken; private Long startTime; - private Long expiryTime = Long.MAX_VALUE; // TODO: + private Long expiryTime = Long.MIN_VALUE; // 'expired' unless set @Override public int hashCode() @@ -144,19 +144,19 @@ public class LockEntity } /** - * @return Returns the ID of the lock holder + * @return Returns the token assigned when the lock was created */ - public String getLockHolder() + public String getLockToken() { - return lockHolder; + return lockToken; } /** - * @param lockHolder the ID of the lock holder + * @param lockToken the token assigned when the lock was created */ - public void setLockHolder(String lockHolder) + public void setLockToken(String lockToken) { - this.lockHolder = lockHolder; + this.lockToken = lockToken; } /** diff --git a/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java b/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java index ac3419a3f0..5964daa65c 100644 --- a/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/locks/ibatis/LockDAOImpl.java @@ -24,7 +24,9 @@ */ package org.alfresco.repo.domain.locks.ibatis; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.alfresco.repo.domain.locks.AbstractLockDAOImpl; import org.alfresco.repo.domain.locks.LockEntity; @@ -41,9 +43,13 @@ public class LockDAOImpl extends AbstractLockDAOImpl { private static final Long CONST_LONG_ZERO = new Long(0L); private static final String SELECT_LOCKRESOURCE_BY_QNAME = "select.LockResourceByQName"; + private static final String SELECT_LOCK_BY_ID = "select.LockByID"; + private static final String SELECT_LOCK_BY_KEY = "select.LockByKey"; private static final String SELECT_LOCK_BY_SHARED_IDS = "select.LockBySharedIds"; private static final String INSERT_LOCKRESOURCE = "insert.LockResource"; private static final String INSERT_LOCK = "insert.Lock"; + private static final String UPDATE_LOCK = "update.Lock"; + private static final String UPDATE_EXCLUSIVE_LOCK = "update.ExclusiveLock"; private SqlMapClientTemplate template; @@ -78,25 +84,46 @@ public class LockDAOImpl extends AbstractLockDAOImpl @SuppressWarnings("unchecked") @Override - protected List getLocks(List lockResourceIds) + protected List getLocksBySharedResourceIds(List sharedLockResourceIds) { - List locks = template.queryForList(SELECT_LOCK_BY_SHARED_IDS, lockResourceIds); + List locks = template.queryForList(SELECT_LOCK_BY_SHARED_IDS, sharedLockResourceIds); // Done return locks; } + + @Override + protected LockEntity getLock(Long id) + { + LockEntity lock = new LockEntity(); + lock.setId(id); + lock = (LockEntity) template.queryForObject(SELECT_LOCK_BY_ID, lock); + // Done + return lock; + } + + @Override + protected LockEntity getLock(Long sharedResourceId, Long exclusiveResourceId) + { + LockEntity lock = new LockEntity(); + lock.setSharedResourceId(sharedResourceId); + lock.setExclusiveResourceId(exclusiveResourceId); + lock = (LockEntity) template.queryForObject(SELECT_LOCK_BY_KEY, lock); + // Done + return lock; + } @Override protected LockEntity createLock( Long sharedResourceId, Long exclusiveResourceId, - String lockApplicant, + String lockToken, long timeToLive) { LockEntity lock = new LockEntity(); lock.setVersion(CONST_LONG_ZERO); lock.setSharedResourceId(sharedResourceId); lock.setExclusiveResourceId(exclusiveResourceId); - lock.setLockHolder(lockApplicant); + lock.setLockToken(lockToken); long now = System.currentTimeMillis(); long exp = now + timeToLive; lock.setStartTime(now); @@ -106,4 +133,42 @@ public class LockDAOImpl extends AbstractLockDAOImpl // Done return lock; } + + @Override + protected LockEntity updateLock(LockEntity lockEntity, String lockToken, long timeToLive) + { + LockEntity updateLockEntity = new LockEntity(); + updateLockEntity.setId(lockEntity.getId()); + updateLockEntity.setVersion(lockEntity.getVersion()); + updateLockEntity.incrementVersion(); // Increment the version number + updateLockEntity.setSharedResourceId(lockEntity.getSharedResourceId()); + updateLockEntity.setExclusiveResourceId(lockEntity.getExclusiveResourceId()); + updateLockEntity.setLockToken(lockToken); + long now = System.currentTimeMillis(); + Long exp = now + timeToLive; + updateLockEntity.setStartTime(lockEntity.getStartTime()); // Keep original start time + updateLockEntity.setExpiryTime(exp); // Don't update the start time + template.update(UPDATE_LOCK, updateLockEntity, 1); + // Done + return updateLockEntity; + } + + @Override + protected int updateLocks( + Long exclusiveLockResourceId, + String oldLockToken, + String newLockToken, + long timeToLive) + { + Map params = new HashMap(11); + params.put("exclusiveLockResourceId", exclusiveLockResourceId); + params.put("oldLockToken", oldLockToken); + params.put("newLockToken", oldLockToken); + long now = System.currentTimeMillis(); + Long exp = new Long(now + timeToLive); + params.put("newExpiryTime", exp); + int updateCount = template.update(UPDATE_EXCLUSIVE_LOCK, params); + // Done + return updateCount; + } } diff --git a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java index a702dba23a..8a93c6406c 100644 --- a/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java +++ b/source/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java @@ -54,6 +54,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; import org.springframework.jdbc.UncategorizedSQLException; import com.ibatis.common.jdbc.exception.NestedSQLException; @@ -95,6 +96,7 @@ public class RetryingTransactionHelper ConcurrencyFailureException.class, DeadlockLoserDataAccessException.class, StaleObjectStateException.class, + JdbcUpdateAffectedIncorrectNumberOfRowsException.class, // Similar to StaleObjectState LockAcquisitionException.class, ConstraintViolationException.class, UncategorizedSQLException.class, @@ -137,7 +139,7 @@ public class RetryingTransactionHelper /** * Callback interface - * @author britt + * @author Derek Hulley */ public interface RetryingTransactionCallback {