/**
* ZKLock.java
*
* Copyright 2016 the original author or authors.
*
* We licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.apache.niolex.lock;
import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.niolex.zookeeper.core.ZKConnector;
import org.apache.niolex.zookeeper.core.ZKException;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The DistributedLock powered by Zookeeper.<br><b>
* This implementation is non-reentrant, and you should not share it between different threads.</b>
* Lock instance can be reused, call {@link #releaseLock()} and use it again. <br>If you call the constructor
* {@link #ZKLock(String, int, String)} to create a new instance, you need to call the {@link #close()}
* method on this instance to close the inner ZKConnector when you do not want to use this lock any more.
* <br>If you call constructor {@link #ZKLock(ZKConnector, String)}, the ZKConnector instance will be managed
* by you. Even if you call {@link #close()}, we will not close the ZKConnector instance.
*
* @author <a href="mailto:xiejiyun@foxmail.com">Xie, Jiyun</a>
* @version 1.0.0
* @since 2016-4-14
*/
public class ZKLock extends DistributedLock implements Closeable {
protected static final Logger LOG = LoggerFactory.getLogger(ZKLock.class);
private static final String[] STUB = new String[0];
private final ZKConnector zkConn;
private final String basePath;
private String selfPath = null;
private String watchPath = null;
private boolean closeZKC = false;
/**
* The lock status:
* 0 - not locked / lock released
* 1 - lock initialized
* 2 - lock acquired
*/
private static final int NO_LOCK = 0;
private static final int LOCK_INITIALIZED = 1;
private static final int LOCKED = 2;
private volatile int lockStatus = NO_LOCK;
/**
* The Constructor to create a new ZKLock instance.
* We will create a {@link ZKConnector} inside this method.
*
* @param clusterAddress the zookeeper cluster servers address list
* @param sessionTimeout the zookeeper session timeout in microseconds
* @param basePath the lock base path
* @throws IOException in cases of network failure
* @throws IllegalArgumentException if sessionTimeout is too small
*/
public ZKLock(String clusterAddress, int sessionTimeout, String basePath)
throws IOException, IllegalArgumentException {
this(new ZKConnector(clusterAddress, sessionTimeout), basePath);
closeZKC = true;
}
/**
* The ZKLock Constructor.
*
* @param zkc the zookeeper connector
* @param basePath the lock base path
*/
public ZKLock(ZKConnector zkc, String basePath) {
super();
this.zkConn = zkc;
if (basePath.endsWith("/")) {
basePath = basePath.substring(0, basePath.length() - 1);
}
this.basePath = basePath;
zkc.makeSurePathExists(basePath);
}
/**
* Add authenticate info for this client.
* 添加client的权限认证信息
*
* @param username the user name of this client
* @param password the password of this client
* @see org.apache.niolex.zookeeper.core.ZKConnector#addAuthInfo(java.lang.String, java.lang.String)
*/
public void addAuthInfo(String username, String password) {
zkConn.addAuthInfo(username, password);
}
/**
* This is the override of super method.
* @see org.apache.niolex.lock.DistributedLock#initLock()
*/
@Override
protected void initLock() {
if (lockStatus > NO_LOCK) {
throw new IllegalStateException("Lock not released, Please unlock it first.");
}
selfPath = zkConn.createNode(basePath + "/lock-", null, true, true);
lockStatus = LOCK_INITIALIZED;
}
/**
* Make whole path from the specified child path.
*
* @param child the child path
* @return the whole path
*/
protected String makeWholePath(String child) {
return basePath + "/" + child;
}
/**
* Make child path from the specified whole path.
*
* @param wholePath the whole path
* @return the child path
*/
protected String makeChildPath(String wholePath) {
return wholePath.substring(wholePath.lastIndexOf('/') + 1);
}
/**
* Check the current lock status, and set the {@link #watchPath} properly.
*
* @return true if we got the lock
*/
@Override
protected boolean isLockReady() {
// Invalid usage.
if (selfPath == null) {
throw new IllegalStateException("Lock not initialized or already released.");
}
// Already locked.
if (lockStatus == LOCKED) {
return true;
}
List<String> children = zkConn.getChildren(basePath);
String self = makeChildPath(selfPath);
// Sort children.
String[] arr = children.toArray(STUB);
Arrays.sort(arr);
// Find myself.
int selfIndex = Arrays.binarySearch(arr, self);
if (selfIndex < 0) {
// Self not found. We need to re-init this lock. This maybe due to session expired...
lockStatus = NO_LOCK;
initLock();
return isLockReady();
} else if (selfIndex == 0) {
// Lock acquired. I am holding the lock.
LOG.debug("Lock acquired as header with path [{}].", selfPath);
lockStatus = LOCKED;
return true;
} else {
watchPath = makeWholePath(arr[selfIndex - 1]);
LOG.debug("Lock is not ready, waiting for path [{}], self path [{}].", watchPath, selfPath);
return false;
}
}
/**
* This is the override of super method.
* @see org.apache.niolex.lock.DistributedLock#watchLock()
*/
@Override
protected void watchLock() throws InterruptedException {
if (watchPath == null) {
throw new IllegalStateException("Should not call this method at this moment.");
}
try {
CountDownLatch latch = new CountDownLatch(1);
Stat s = zkConn.zooKeeper().exists(watchPath, new ExistsWather(latch));
if (s == null) {
return;
} else {
latch.await();
}
} catch (KeeperException e) {
throw ZKException.makeInstance("ZKLock internal error.", e);
}
}
/**
* This is the override of super method.
* @see org.apache.niolex.lock.DistributedLock#watchLock(long, java.util.concurrent.TimeUnit)
*/
@Override
protected boolean watchLock(long timeout, TimeUnit unit) throws InterruptedException {
if (watchPath == null) {
throw new IllegalStateException("Should not call this method at this moment.");
}
try {
CountDownLatch latch = new CountDownLatch(1);
Stat s = zkConn.zooKeeper().exists(watchPath, new ExistsWather(latch));
if (s == null) {
return true;
} else {
return latch.await(timeout, unit);
}
} catch (KeeperException e) {
throw ZKException.makeInstance("ZKLock internal error.", e);
}
}
/**
* This is the override of super method.
* @see org.apache.niolex.lock.DistributedLock#releaseLock()
*/
@Override
protected void releaseLock() {
if (selfPath != null) {
zkConn.deleteNode(selfPath);
if (lockStatus == LOCKED)
LOG.debug("Lock released with path [{}].", selfPath);
}
lockStatus = NO_LOCK;
selfPath = null;
watchPath = null;
}
/**
* Check the exists of the watched node.
*
* @author <a href="mailto:xiejiyun@foxmail.com">Xie, Jiyun</a>
* @version 1.0.0
* @since 2016-4-14
*/
public class ExistsWather implements Watcher {
// The latch used to wait for the lock to be ready.
private final CountDownLatch latch;
/**
* Constructor.
*
* @param latch the latch used to notify event
*/
public ExistsWather(CountDownLatch latch) {
super();
this.latch = latch;
}
/**
* This is the override of super method.
* @see org.apache.zookeeper.Watcher#process(org.apache.zookeeper.WatchedEvent)
*/
@Override
public void process(WatchedEvent event) {
// Each watch can be triggered only once.
// We need to make sure do this thing right.
try {
if (event.getType() == Watcher.Event.EventType.None) {
// If event type is none, then something wrong with the zookeeper.
while (!zkConn.connected())
zkConn.waitForConnectedTillDeath();
}
} finally {
latch.countDown();
}
}
}
/**
* @return the current lock status
*/
public boolean locked() {
return lockStatus == LOCKED;
}
/**
* Close the internal zookeeper connector if it's created by this class, otherwise just release this lock.
*/
@Override
public void close() {
if (closeZKC) {
zkConn.close();
} else {
releaseLock();
}
}
}