/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2015 ForgeRock AS */ package org.opends.server.types; import static org.assertj.core.api.Assertions.assertThat; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.opends.server.TestCaseUtils; import org.opends.server.types.LockManager.DNLock; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @Test(timeOut = 20000, sequential = true) @SuppressWarnings("javadoc") public class LockManagerTest extends TypesTestCase { private enum LockType { READ_ENTRY { @Override DNLock lock(final LockManager lockManager, final DN dn) { return lockManager.tryReadLockEntry(dn); } }, WRITE_ENTRY { @Override DNLock lock(final LockManager lockManager, final DN dn) { return lockManager.tryWriteLockEntry(dn); } }, WRITE_SUBTREE { @Override DNLock lock(final LockManager lockManager, final DN dn) { return lockManager.tryWriteLockSubtree(dn); } }; abstract DNLock lock(LockManager lockManager, DN dn); } private DN dnA; private DN dnAB; private DN dnABC; private DN dnABD; private final ExecutorService thread1 = Executors.newSingleThreadExecutor(); private final ExecutorService thread2 = Executors.newSingleThreadExecutor(); @BeforeClass private void setup() throws Exception { TestCaseUtils.startServer(); dnA = DN.valueOf("dc=a"); dnAB = DN.valueOf("dc=b,dc=a"); dnABC = DN.valueOf("dc=c,dc=b,dc=a"); dnABD = DN.valueOf("dc=d,dc=b,dc=a"); } @Test public void testLockTimeout() throws Exception { final LockManager lockManager = new LockManager(100, TimeUnit.MILLISECONDS); DNLock lock1 = lockUsingThread(thread1, lockManager, LockType.WRITE_ENTRY, dnABC).get(); DNLock lock2 = lockUsingThread(thread2, lockManager, LockType.WRITE_ENTRY, dnABC).get(); assertThat(lock1).isNotNull(); assertThat(lock2).isNull(); // Timed out. unlockUsingThread(thread1, lock1); } @DataProvider private Object[][] multiThreadedLockCombinationsWhichShouldBlock() { // @formatter:off return new Object[][] { { LockType.READ_ENTRY, dnA, LockType.WRITE_ENTRY, dnA }, { LockType.WRITE_ENTRY, dnA, LockType.READ_ENTRY, dnA }, { LockType.WRITE_ENTRY, dnA, LockType.WRITE_ENTRY, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.READ_ENTRY, dnA }, { LockType.READ_ENTRY, dnA, LockType.WRITE_SUBTREE, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.WRITE_ENTRY, dnA }, { LockType.WRITE_ENTRY, dnA, LockType.WRITE_SUBTREE, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.WRITE_SUBTREE, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.READ_ENTRY, dnAB }, { LockType.READ_ENTRY, dnAB, LockType.WRITE_SUBTREE, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.WRITE_ENTRY, dnAB }, { LockType.WRITE_ENTRY, dnAB, LockType.WRITE_SUBTREE, dnA }, { LockType.WRITE_SUBTREE, dnA, LockType.WRITE_SUBTREE, dnAB }, { LockType.WRITE_SUBTREE, dnAB, LockType.WRITE_SUBTREE, dnA }, }; // @formatter:on } @Test(dataProvider = "multiThreadedLockCombinationsWhichShouldBlock") public void testMultiThreadedLockCombinationsWhichShouldBlock(final LockType lock1Type, final DN dn1, final LockType lock2Type, final DN dn2) throws Exception { final LockManager lockManager = new LockManager(); final DNLock lock1 = lockUsingThread(thread1, lockManager, lock1Type, dn1).get(); final Future<DNLock> lock2Future = lockUsingThread(thread2, lockManager, lock2Type, dn2); try { lock2Future.get(10, TimeUnit.MILLISECONDS); } catch (final TimeoutException e) { // Ignore: we'll check the state of the future instead. } assertThat(lock2Future.isDone()).isFalse(); unlockUsingThread(thread1, lock1); final DNLock lock2 = lock2Future.get(); unlockUsingThread(thread2, lock2); assertThat(getThreadLocalLockRefCountFor(thread1, lockManager, dn1)).isGreaterThan(0); assertThat(getThreadLocalLockRefCountFor(thread2, lockManager, dn2)).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn1)).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn2)).isGreaterThan(0); } @DataProvider private Object[][] multiThreadedLockCombinationsWhichShouldNotBlock() { // @formatter:off return new Object[][] { { LockType.READ_ENTRY, dnA, LockType.READ_ENTRY, dnA }, { LockType.READ_ENTRY, dnA, LockType.WRITE_ENTRY, dnAB }, { LockType.WRITE_ENTRY, dnAB, LockType.READ_ENTRY, dnA }, { LockType.WRITE_ENTRY, dnA, LockType.WRITE_ENTRY, dnAB }, { LockType.WRITE_ENTRY, dnAB, LockType.WRITE_ENTRY, dnA }, { LockType.READ_ENTRY, dnA, LockType.WRITE_SUBTREE, dnAB }, { LockType.WRITE_SUBTREE, dnAB, LockType.READ_ENTRY, dnA }, { LockType.WRITE_ENTRY, dnA, LockType.WRITE_SUBTREE, dnAB }, { LockType.WRITE_SUBTREE, dnAB, LockType.WRITE_ENTRY, dnA }, { LockType.WRITE_SUBTREE, dnABC, LockType.WRITE_SUBTREE, dnABD }, }; // @formatter:on } @Test(dataProvider = "multiThreadedLockCombinationsWhichShouldNotBlock") public void testMultiThreadedLockCombinationsWhichShouldNotBlock(final LockType lock1Type, final DN dn1, final LockType lock2Type, final DN dn2) throws Exception { final LockManager lockManager = new LockManager(); final DNLock lock1 = lockUsingThread(thread1, lockManager, lock1Type, dn1).get(); final DNLock lock2 = lockUsingThread(thread2, lockManager, lock2Type, dn2).get(); assertThat(lock1).isNotSameAs(lock2); unlockUsingThread(thread1, lock1); unlockUsingThread(thread2, lock2); assertThat(getThreadLocalLockRefCountFor(thread1, lockManager, dn1)).isGreaterThan(0); assertThat(getThreadLocalLockRefCountFor(thread2, lockManager, dn2)).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn1)).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn2)).isGreaterThan(0); } @DataProvider private LockType[][] reentrantLockCombinationsWhichShouldNotBlock() { // @formatter:off return new LockType[][] { { LockType.READ_ENTRY, LockType.READ_ENTRY }, { LockType.WRITE_ENTRY, LockType.READ_ENTRY }, { LockType.WRITE_ENTRY, LockType.WRITE_ENTRY }, { LockType.WRITE_SUBTREE, LockType.READ_ENTRY }, { LockType.WRITE_SUBTREE, LockType.WRITE_ENTRY }, { LockType.WRITE_SUBTREE, LockType.WRITE_SUBTREE }, }; // @formatter:on } @Test(dataProvider = "reentrantLockCombinationsWhichShouldNotBlock") public void testReentrantLockCombinationsWhichShouldNotBlock(final LockType lock1Type, final LockType lock2Type) { final LockManager lockManager = new LockManager(); final DNLock lock1 = lock1Type.lock(lockManager, dnA); final DNLock lock2 = lock2Type.lock(lockManager, dnA); assertThat(lock1).isNotSameAs(lock2); assertThat(lock1.refCount()).isEqualTo(3); // +1 for thread local cache assertThat(lock2.refCount()).isEqualTo(3); lock1.unlock(); assertThat(lock1.refCount()).isEqualTo(2); assertThat(lock2.refCount()).isEqualTo(2); lock2.unlock(); assertThat(lock1.refCount()).isEqualTo(1); assertThat(lock2.refCount()).isEqualTo(1); assertThat(lockManager.getThreadLocalCacheRefCountFor(dnA)).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dnA)).isGreaterThan(0); } @Test public void testThreadLocalCacheEviction() throws Exception { final LockManager lockManager = new LockManager(); // Acquire 100 different locks. The first few locks should be evicted from the cache. final LinkedList<DNLock> locks = new LinkedList<>(); for (int i = 0; i < 100; i++) { locks.add(lockManager.tryWriteLockEntry(dn(i))); } // The first lock should have been evicted from the cache, but still in the lock table because it is locked. assertThat(locks.getFirst().refCount()).isEqualTo(1); assertThat(lockManager.getThreadLocalCacheRefCountFor(dn(0))).isLessThan(0); assertThat(lockManager.getLockTableRefCountFor(dn(0))).isGreaterThan(0); // The last lock should still be in the cache and the lock table. assertThat(locks.getLast().refCount()).isEqualTo(2); assertThat(lockManager.getThreadLocalCacheRefCountFor(dn(99))).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn(99))).isGreaterThan(0); for (final DNLock lock : locks) { lock.unlock(); } // The first lock should not be in the cache or the lock table. assertThat(locks.getFirst().refCount()).isEqualTo(0); assertThat(lockManager.getThreadLocalCacheRefCountFor(dn(0))).isLessThan(0); assertThat(lockManager.getLockTableRefCountFor(dn(0))).isLessThan(0); // The last lock should still be in the cache and the lock table. assertThat(locks.getLast().refCount()).isEqualTo(1); assertThat(lockManager.getThreadLocalCacheRefCountFor(dn(99))).isGreaterThan(0); assertThat(lockManager.getLockTableRefCountFor(dn(99))).isGreaterThan(0); } @Test(description = "OPENDJ-1984") public void stressTestForDeadlocks() throws Exception { final LockManager lockManager = new LockManager(); final int threadCount = Runtime.getRuntime().availableProcessors(); final ExecutorService threadPool = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { threadPool.submit(new Runnable() { @Override public void run() { final Random rng = new Random(); for (int j = 0; j < 1000000; j++) { try { /* * Lock a DN whose parent is different each time in order to prevent the thread local * cache being used for retrieving the parent DN lock. */ final int uid = rng.nextInt(); final int deviceId = rng.nextInt(); final DN dn = DN.valueOf("uid=" + deviceId + ",uid=" + uid + ",dc=example,dc=com"); lockManager.tryWriteLockEntry(dn).unlock(); } catch (DirectoryException e) { throw new RuntimeException(e); } } } }); } threadPool.shutdown(); assertThat(threadPool.awaitTermination(60, TimeUnit.SECONDS)).as("Deadlock detected during stress test").isTrue(); } private DN dn(final int i) throws DirectoryException { return DN.valueOf(String.format("uid=user.%d,ou=people,dc=example,dc=com", i)); } private Future<DNLock> lockUsingThread(final ExecutorService thread, final LockManager lockManager, final LockType lockType, final DN dn) throws Exception { return thread.submit(new Callable<DNLock>() { @Override public DNLock call() throws Exception { return lockType.lock(lockManager, dn); } }); } private int getThreadLocalLockRefCountFor(final ExecutorService thread, final LockManager lockManager, final DN dn) throws Exception { return thread.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { return lockManager.getThreadLocalCacheRefCountFor(dn); } }).get(); } private void unlockUsingThread(final ExecutorService thread, final DNLock lock) throws Exception { thread.submit(new Runnable() { @Override public void run() { lock.unlock(); } }).get(); } }