Class ReentrantDistributedZookeeperLock
- java.lang.Object
-
- com.broadleafcommerce.search.provider.solr.util.zk.ReentrantDistributedZookeeperLock
-
- All Implemented Interfaces:
com.broadleafcommerce.search.api.DistributedLock
,Lock
public class ReentrantDistributedZookeeperLock extends Object implements com.broadleafcommerce.search.api.DistributedLock
This is similar to aReentrantLock
, except that it uses Zookeeper to share the lock state across multiple nodes, allowing for a distributed lock. Since we exclusively use Apache SolrCloud which relies on Apache ZooKeeper we can assume that if this library is on the classpath that ZooKeeper will be available as well. This is a general purpose, distributed locking mechanism that implementsLock
. The owning thread may acquire the lock multiple times, but must unlock for each time the lock is acquired. It's important to get in the habit of unlocking immediately in a finally block to prevent orphaned locks:Lock lock = new ReentrantDistributedZookeeperLock(zk, "/solr-update/locks", "solrUpdate_commandLock"); lock.lockInterruptibly(); //This will block until a lock is acquired or until the Thread is interrupted. try { // Arbitrarily lock again for demonstration purposes. lock.lock() try { //Do something in a globally locked state } finally { // Be sure to unlock each time a lock is acquired. lock.unlock(); } } finally { // Be sure to unlock each time a lock is acquired. lock.unlock(); }
or
Lock lock = new ReentrantDistributedZookeeperLock(zk, "/solr-update/locks", "solrUpdate_commandLock"); if (lock.tryLock()) { try { //Do something in a globally locked state } finally { lock.unlock(); } }
In terms of performance, the average lock time, on a single quad-core laptop running a single Zookeeper instance, takes about 7-8 milliseconds. And the average unlock time also takes around 7-8 milliseconds during a simple test on a quad core laptop that was also running a single Zookeeper instance. This is with a single thread, and is assuming no reentrant locks and no contention or waiting for locks to become available. In other words, disregarding blocking and contention, it will likely take, on average, 14-16 milliseconds to create and release a lock (approximately 55-65 locks and releases / second for any given lock). These metrics will depend on a number of things, including contention for locks, Zookeeper cluster size and configuration, hardware, etc. Note that this locking mechanism attempts to be fair, but does not explicitly guarantee fairness.- Author:
- Kelly Tisdell (ktisdell)
-
-
Field Summary
Fields Modifier and Type Field Description static String
DEFAULT_BASE_FOLDER
This is the base folder that all locks will be written to in Zookeeper.static String
LOCK_PREFIX
This is the prefix of any lock entry.
-
Constructor Summary
Constructors Constructor Description ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName)
ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, boolean useDefaultBasePath, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), and the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_').ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), and the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_').ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, org.springframework.core.env.Environment env, boolean useDefaultBasePath, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'), and anEnvironment
object, which can be null.ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, org.springframework.core.env.Environment env, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'), and anEnvironment
object, which can be null.
-
Method Summary
All Methods Instance Methods Concrete Methods Modifier and Type Method Description boolean
canParticipate()
Allows one to disable this locking mechanism via the 'com.broadleafcommerce.search.api.DistributedLock.canParticipate' (globally) or the 'com.broadleafcommerce.search.api.DistributedLock.${lockName}.canParticipate' property.boolean
currentThreadHoldsLock()
protected Optional<String>
findFirstLockFileName(String currentLockPath)
Reads all of the lock files under the lock folder and orders them by file name.protected String
generateLockFile()
Create a lock reference in Zookeeper.protected List<org.apache.zookeeper.data.ACL>
getAcls()
Returns a list of Zookeeper ACLs to be used for this lock.int
getCurrentThreadLockPermits()
protected org.springframework.core.env.Environment
getEnvironment()
Returns the SpringEnvironment
object associated with this environment.protected int
getFailureRetries()
This will retry any interactions with Zookeeper this many times.protected String
getFullLockName()
Returns LOCK_PREFIX + getLockName() E.g.protected String
getLockAccessPropertyName()
The name of the property associated with this lock.protected String
getLockFolderPath()
protected String
getLockName()
The lock name provided in the constructor.protected long
getRetryWaitTime()
If an interaction with Zookeeper fails, it will retry and this will be the pause time, in millis, between retries.protected org.apache.zookeeper.ZooKeeper
getZookeeperClient()
protected Comparator<String>
getZooKeeperSequentialComparator(String lockFileName)
This sorts the ZooKeeper files (znodes) according to name, based on the appended sequence, and depending on what the current sequence is.protected void
initialize()
Creates the appropriate folder(s) in Zookeeper if they don't already exist.protected boolean
isAdditiveWaitTimes()
If this is true, the wait time in millis will be incremented by itself each time a retry occurs, up to the retry count.boolean
isLocked()
Indicates whether, at the time of calling, a distributed lock is in place.void
lock()
protected boolean
lockInternally(long waitTime)
Negative number means wait indefinitely (typically until anInterruptedException
is thrown.void
lockInterruptibly()
Condition
newCondition()
protected int
parseIntegerSequenceFromFilename(String lockFileName)
This parses the 32-bit signed integer from the end of the lock file name (or znode).boolean
tryLock()
boolean
tryLock(long time, TimeUnit unit)
void
unlock()
-
-
-
Field Detail
-
DEFAULT_BASE_FOLDER
public static final String DEFAULT_BASE_FOLDER
This is the base folder that all locks will be written to in Zookeeper. The constructors require a lock path, which will be appended to this path.- See Also:
- Constant Field Values
-
LOCK_PREFIX
public static final String LOCK_PREFIX
This is the prefix of any lock entry. It is dzlck-. A lock file (node) will be dzlck-mylock, for example.- See Also:
- Constant Field Values
-
-
Constructor Detail
-
ReentrantDistributedZookeeperLock
public ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName)
-
ReentrantDistributedZookeeperLock
public ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), and the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'). ThisLock
will, by default, participate in or be allowed to acquire a lock. The lockPath will be prepended with '/broadleaf/app/distributed-locks'.- Parameters:
zk
- TheZooKeeper
clientlockPath
- The path to the lock file (similar to a file system path)lockName
- The name of the lock, itself (e.g. "CATALOG")acls
- The ZooKeeper security ACLs
-
ReentrantDistributedZookeeperLock
public ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, boolean useDefaultBasePath, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), and the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'). ThisLock
will, by default, participate in or be allowed to acquire a lock. If use defaultBasePath is true, then the lockPath will be prepended with '/broadleaf/app/distributed-locks'.- Parameters:
zk
- TheZooKeeper
clientlockPath
- The path to the lock file (similar to a file system path)lockName
- The name of the lock, itself (e.g. "CATALOG")acls
- The ZooKeeper security ACLs
-
ReentrantDistributedZookeeperLock
public ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, org.springframework.core.env.Environment env, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'), and anEnvironment
object, which can be null. ThisLock
will, by default, participate in or be allowed to acquire a lock if theEnvironment
argument is null or if the 'com.broadleafcommerce.search.api.DistributedLock.${lockName}.canParticipate' property is not set or is set to false. The lockPath will be prepended with '/broadleaf/app/distributed-locks'.- Parameters:
zk
- TheZooKeeper
clientlockPath
- The path to the lock file (similar to a file system path)lockName
- The name of the lock, itself (e.g. "CATALOG")env
- The (optional) SpringEnvironment
instanceacls
- The ZooKeeper security ACLs
-
ReentrantDistributedZookeeperLock
public ReentrantDistributedZookeeperLock(org.apache.zookeeper.ZooKeeper zk, String lockPath, String lockName, org.springframework.core.env.Environment env, boolean useDefaultBasePath, List<org.apache.zookeeper.data.ACL> acls)
This constructor takes in theZooKeeper
(non-nullable), the lock path (non-nullable and in the format of '/path/to/this/lock/folder`), the lock name (non-nullable and non-empty string with no whitespaces, leading or trailing slashes, or special characters except '-' or '_'), and anEnvironment
object, which can be null. ThisLock
will, by default, participate in or be allowed to acquire a lock if theEnvironment
argument is null or if the 'com.broadleafcommerce.search.api.${lockName}.canParticipate' property is not set or is set to false. If use defaultBasePath is true, then the lockPath will be prepended with '/broadleaf/app/distributed-locks'.- Parameters:
zk
- TheZooKeeper
clientlockPath
- The path to the lock file (similar to a file system path)lockName
- The name of the lock, itself (e.g. "CATALOG")env
- The (optional) SpringEnvironment
instanceuseDefaultBasePath
- Indicates if a default path should be used (e.g.DEFAULT_BASE_FOLDER
.acls
- The ZooKeeper security ACLs
-
-
Method Detail
-
lockInterruptibly
public void lockInterruptibly() throws InterruptedException
- Specified by:
lockInterruptibly
in interfaceLock
- Throws:
InterruptedException
-
tryLock
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
- Specified by:
tryLock
in interfaceLock
- Throws:
InterruptedException
-
newCondition
public Condition newCondition()
- Specified by:
newCondition
in interfaceLock
-
lockInternally
protected boolean lockInternally(long waitTime) throws InterruptedException
Negative number means wait indefinitely (typically until anInterruptedException
is thrown. Zero (0) means don't wait. Positive number means wait for that number of millis.- Parameters:
waitTime
- time to wait for a lock- Throws:
InterruptedException
-
initialize
protected void initialize()
Creates the appropriate folder(s) in Zookeeper if they don't already exist.
-
currentThreadHoldsLock
public boolean currentThreadHoldsLock()
- Specified by:
currentThreadHoldsLock
in interfacecom.broadleafcommerce.search.api.DistributedLock
-
getCurrentThreadLockPermits
public int getCurrentThreadLockPermits()
-
isLocked
public boolean isLocked()
Indicates whether, at the time of calling, a distributed lock is in place. Note that this is more like a hint because as soon as this method exits, another thread may create a lock.- Returns:
- whether this lock is locked by this thread or another.
-
canParticipate
public boolean canParticipate()
Allows one to disable this locking mechanism via the 'com.broadleafcommerce.search.api.DistributedLock.canParticipate' (globally) or the 'com.broadleafcommerce.search.api.DistributedLock.${lockName}.canParticipate' property. Both are true by default. If either of these properties are set to false, then this lock will never be obtained. This allows only certain environments or certain nodes (JVMs) to obtain the lock while preventing others. Assuming theEnvironment
is not null, this first checks the more specific 'com.broadleafcommerce.search.api.DistributedLock.${lockName}.canParticipate' property. If that is true or null, then it checks the more global 'com.broadleafcommerce.search.api.DistributedLock.canParticipate'. If that is true or null then it returns true. By default, this method returns true if theEnvironment
is null or if both properties are null. If this method returns false, then the locking mechanisms and semantics will continue to work. However, the lock will always be locked and will never allow the acquisition of a lock.- Specified by:
canParticipate
in interfacecom.broadleafcommerce.search.api.DistributedLock
- Returns:
- whether this environment can participate in this lock, as specified by name.
-
getZookeeperClient
protected org.apache.zookeeper.ZooKeeper getZookeeperClient()
-
getLockName
protected String getLockName()
The lock name provided in the constructor.- Returns:
-
getFullLockName
protected String getFullLockName()
Returns LOCK_PREFIX + getLockName() E.g. dzlck-myLock- Returns:
-
getLockFolderPath
protected String getLockFolderPath()
-
getFailureRetries
protected int getFailureRetries()
This will retry any interactions with Zookeeper this many times. Must be a positive number. Zero (0) is OK.- Returns:
-
getRetryWaitTime
protected long getRetryWaitTime()
If an interaction with Zookeeper fails, it will retry and this will be the pause time, in millis, between retries.- Returns:
- time between reties in milliseconds
-
isAdditiveWaitTimes
protected boolean isAdditiveWaitTimes()
If this is true, the wait time in millis will be incremented by itself each time a retry occurs, up to the retry count. So, if the retries is 5 and the retryWaitTime is 100, then the max amount of time that the system will wait is 1500: 100 + 200 + 300 + 400 + 500, not including any wait time associated with I/O or execution of the Operation.- Returns:
- whether wait times should be added together between each retry
-
getAcls
protected List<org.apache.zookeeper.data.ACL> getAcls()
Returns a list of Zookeeper ACLs to be used for this lock.- Returns:
- the ZooKeeper ACL list
-
getEnvironment
protected org.springframework.core.env.Environment getEnvironment()
Returns the SpringEnvironment
object associated with this environment. This method may return null.- Returns:
- the current Spring
Environment
, or null.
-
getLockAccessPropertyName
protected String getLockAccessPropertyName()
The name of the property associated with this lock. Used by canParticipate method to determine if this thisEnvironment
can obtain access to (i.e. lock) thisLock
.- Returns:
-
generateLockFile
protected String generateLockFile() throws Exception
Create a lock reference in Zookeeper. By default, it looks something like /path/to/my/locks/dzlck-myLockxyz, where 'xyz' is a signed 32-bit integer. Creating this file does not guarantee that a lock has been acquired. It is simply a placeholder for this thread competing against others for lock acquisition. The sequence appended to the lock name allows us to determine which file was created first and therefore, which thread (the one who created the file) holds the lock.- Returns:
- a full file name, including the path, of the lock file handle that this thread is using to compete for a lock
- Throws:
Exception
-
findFirstLockFileName
protected Optional<String> findFirstLockFileName(String currentLockPath) throws Exception
Reads all of the lock files under the lock folder and orders them by file name. Then, this selects the first file, which is the one whose thread owns the lock.- Parameters:
currentLockPath
- The path of the lock file created by this thread- Returns:
- the file name of the lock file whose name is first in a list, ordered by name lexicographically
- Throws:
Exception
-
getZooKeeperSequentialComparator
protected Comparator<String> getZooKeeperSequentialComparator(String lockFileName)
This sorts the ZooKeeper files (znodes) according to name, based on the appended sequence, and depending on what the current sequence is. ZooKeeper allows us to use a built-in sequence generator to append a sequence to the lock file (or znode). This helps us sort the znodes by name, with the first name being associated with the znode that has the lock. Threads compete to obtain a lock. The thread that created a file with a sequence that is sorted before another thread that created a file with a different sequence wins. The problem is that it's a 32 bit signed sequence that starts at zero and overflows toInteger.MIN_VALUE
after it reachesInteger.MAX_VALUE
. We need to sort positive numbers first after we roll over toInteger.MIN_VALUE
, and negative numbers first after we pass zero again.- Parameters:
lockFileName
- The file name created by the current thread.- Returns:
- A comparator that sorts files by their 32-bit file name appendage.
-
parseIntegerSequenceFromFilename
protected final int parseIntegerSequenceFromFilename(String lockFileName)
This parses the 32-bit signed integer from the end of the lock file name (or znode).- Parameters:
lockFileName
- The name of a lock file for which to parse the sequence number- Returns:
- The signed, 32-bit integer which is a sequence number generated by ZooKeeper.
-
-