Class LockManager


  • @PublicAPI(stability=UNCOMMITTED,
               mayInstantiate=false,
               mayExtend=false,
               mayInvoke=true)
    public final class LockManager
    extends Object
    A lock manager coordinates directory update operations so that the DIT structure remains in a consistent state, as well as providing repeatable read isolation. When accessing entries components need to ensure that they have the appropriate lock:
    • repeatable reads: repeatable read isolation is rarely needed in practice, since all backend reads are guaranteed to be performed with read-committed isolation, which is normally sufficient. Specifically, read-only operations such as compare and search do not require any additional locking. If repeatable read isolation is required then lock the entry using tryReadLockEntry(Dn)
    • modifying an entry: acquire an entry write-lock for the target entry using tryWriteLockEntry(Dn). Updates are typically performed using a read-modify-write cycle, so the write lock should be acquired before performing the initial read in order to ensure consistency
    • adding an entry: client code must acquire an entry write-lock for the target entry using tryWriteLockEntry(Dn). The parent entry will automatically be protected from deletion by an implicit subtree read lock on the parent
    • deleting an entry: client code must acquire a subtree write lock for the target entry using tryWriteLockSubtree(Dn)
    • renaming an entry: client code must acquire a subtree write lock for the old entry, and a subtree write lock for the new entry using tryWriteLockSubtree(Dn). Care should be taken to avoid deadlocks, e.g. by locking the DN which sorts first.
    In addition, backend implementations may choose to use their own lock manager for enforcing atomicity and isolation. This is typically the case for backends which cannot take advantage of atomicity guarantees provided by an underlying DB (the task backend is one such example).

    Implementation Notes

    The lock table is conceptually a cache of locks keyed on DN, i.e. a Map<DN, DNLock>. Locks must be kept in the cache while they are locked, but may be removed once they are no longer locked by any threads. Locks are represented using a pair of read-write locks: the first lock is the "subtree" lock and the second is the "entry" lock.

    In order to lock an entry for read or write a subtree read lock is first acquired on each of the parent entries from the empty DN down to the immediate parent of the entry to be locked. Then the appropriate read or write entry lock is acquired for the target entry. Subtree write locking is performed by acquiring a subtree read lock on each of the parent entries from the empty DN down to the immediate parent of the subtree to be locked. Then a subtree write lock is acquired for the target subtree.

    The lock table itself is not represented using a ConcurrentHashMap because the JDK6/7 APIs do not provide the ability to atomically add-and-lock or unlock-and-remove locks (this capability is provided in JDK8). Instead, we provide our own implementation comprising of a fixed number of buckets, a bucket being a LinkedList of DNLocks. In addition, it is important to be able to efficiently iterate up and down a chain of hierarchically related locks, so each lock maintains a reference to its parent lock. Modern directories tend to have a flat structure so it is also important to avoid contention on "hot" parent DNs. Typically, a lock attempt against a DN will involve a cache miss for the target DN and a cache hit for the parent, but the parent will be the same parent for all lock requests, resulting in a lot of contention on the same lock bucket. To avoid this the lock manager maintains a small-thread local cache of locks, so that parent locks can be acquired using a lock-free algorithm.

    Since the thread local cache may reference locks which are not actively locked by anyone, a reference counting mechanism is used in order to prevent cached locks from being removed from the underlying lock table. The reference counting mechanism is also used for references between a lock and its parent lock. To summarize, locking a DN involves the following steps:

    • get the lock from the thread local cache. If the lock was not in the thread local cache then try fetching it from the lock table:
      • found - store it in the thread local cache and bump the reference count
      • not found - create a new lock. First fetch the parent lock using the same process, i.e. looking in the thread local cache, etc. Then create a new lock referencing the parent lock (bumps the reference count for the parent lock), and store it in the lock table and the thread local cache with a reference count of 1.
    • return the lock to the application and increment its reference count since the application now also has a reference to the lock.
    Locks are dereferenced when they are unlocked, when they are evicted from a thread local cache, and when a child lock's reference count reaches zero. A lock is completely removed from the lock table once its reference count reaches zero.
    • Constructor Detail

      • LockManager

        public LockManager()
        Creates a new lock manager with a lock timeout of 9 seconds and an automatically chosen number of lock table buckets based on the number of processors.
      • LockManager

        public LockManager​(long lockTimeout,
                           TimeUnit lockTimeoutUnit)
        Creates a new lock manager with the specified lock timeout and an automatically chosen number of lock table buckets based on the number of processors.
        Parameters:
        lockTimeout - The lock timeout.
        lockTimeoutUnit - The lock timeout units.
    • Method Detail

      • tryReadLockEntry

        public LockManager.DNLock tryReadLockEntry​(Dn entryDn)
        Acquires the read lock for the specified entry DN. This method will block if the entry is already write locked or if the entry, or any of its parents, have the subtree write lock taken.
        Parameters:
        entryDn - The entry DN whose read lock is required.
        Returns:
        The lock, or null if the lock attempt timed out.
      • tryWriteLockEntry

        public LockManager.DNLock tryWriteLockEntry​(Dn entryDn)
        Acquires the write lock for the specified entry DN. This method will block if the entry is already read or write locked or if the entry, or any of its parents, have the subtree write lock taken.
        Parameters:
        entryDn - The entry DN whose write lock is required.
        Returns:
        The lock, or null if the lock attempt timed out.
      • tryWriteLockSubtree

        public LockManager.DNLock tryWriteLockSubtree​(Dn subtreeDn)
        Acquires the write lock for the specified subtree DN. This method will block if any entry or subtree within the subtree is already read or write locked or if any of the parent entries of the subtree have the subtree write lock taken.
        Parameters:
        subtreeDn - The subtree DN whose write lock is required.
        Returns:
        The lock, or null if the lock attempt timed out.
      • clear

        public void clear()
        Clears the locks table.