/*
* Copyright 2015-2016 OpenCB
*
* Licensed 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.opencb.opencga.storage.hadoop.utils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.TimeoutException;
/**
* Concurrent lock using an HBase cell.
*
* Basic algorithm:
*
* Lock:
* String token = Random();
* HBase.append(row, column, token);
* if (HBase.get(row, column).startsWith(token)) {
* // Win the token
* return TRUE;
* } else {
* // Token already taken
* return FALSE;
* }
*
* Unlock:
* HBase.put(row, column, "");
*
* Created on 19/05/16.
*
* @author Jacobo Coll <jacobo167@gmail.com>
*/
public class HBaseLock {
private static final String LOCK_SEPARATOR = "_";
private static final String LOCK_EXPIRING_DATE_SEPARATOR = ":";
private static final String CURRENT_LOCK = "CURRENT-";
protected final HBaseManager hbaseManager;
protected final String tableName;
protected final byte[] columnFamily;
protected final byte[] studiesRow;
protected static Logger logger = LoggerFactory.getLogger(HBaseLock.class);
public HBaseLock(HBaseManager hbaseManager, String tableName, byte[] columnFamily, byte[] row) {
this.hbaseManager = hbaseManager;
this.tableName = tableName;
this.columnFamily = columnFamily;
this.studiesRow = row;
}
/**
* Apply for the lock.
*
* @param column Column to find the lock cell
* @param lockDuration Duration un milliseconds of the token. After this time the token is expired.
* @param timeout Max time in milliseconds to wait for the lock
*
* @return Lock token
*
* @throws InterruptedException if any thread has interrupted the current thread.
* @throws TimeoutException if the operations takes more than the timeout value.
* @throws IOException if there is an error writing or reading from HBase.
*/
public long lock(byte[] column, long lockDuration, long timeout)
throws InterruptedException, TimeoutException, IOException {
String token = RandomStringUtils.randomAlphanumeric(10);
// Minimum lock duration of 100ms
lockDuration = Math.max(lockDuration, 100);
String[] lockValue;
String readToken = "";
StopWatch stopWatch = new StopWatch();
stopWatch.start();
lockValue = readLockValue(column);
do {
// If the lock is taken, wait
while (isLockTaken(lockValue)) {
Thread.sleep(100);
lockValue = readLockValue(column);
//Check if the lock is still valid
if (stopWatch.getTime() > timeout) {
throw new TimeoutException("Unable to get the lock");
}
}
//Check if the lock is still valid
if (stopWatch.getTime() > timeout) {
throw new TimeoutException("Unable to get the lock");
}
// Append token to the lock cell
appendToken(token, lockDuration, column);
lockValue = readLockValue(column);
// Get the first non expired lock
for (String lock : lockValue) {
if (!isLockExpired(lock)) {
readToken = lock.split(LOCK_EXPIRING_DATE_SEPARATOR)[0];
break;
}
}
// You win the lock if the first available lock is yours.
} while (!readToken.equals(token));
logger.info("Won the lock with token " + token + " (" + token.hashCode() + ") from lock: " + Arrays.toString(lockValue));
// Overwrite the lock with the winner current lock. Remove previous expired locks
putCurrentLock(token, lockDuration, column);
return token.hashCode();
}
/**
* Releases the lock.
*
* @param column Column to find the lock cell
* @param lockToken Lock token
* @throws IOException if there is an error writing or reading from HBase.
* @throws IllegalStateException if the lockToken does not match with the current lockToken
*/
public void unlock(byte[] column, long lockToken) throws IOException {
String[] lockValue;
lockValue = readLockValue(column);
String currentLock = "";
for (String lock : lockValue) {
if (lock.startsWith(CURRENT_LOCK)) {
currentLock = lock.replace(CURRENT_LOCK, "").split(LOCK_EXPIRING_DATE_SEPARATOR)[0];
break;
}
}
if (currentLock.hashCode() != lockToken) {
throw new IllegalStateException("Inconsistent lock status. You don't have the lock!"
+ lockToken + " != " + currentLock.hashCode() + " from " + Arrays.toString(lockValue));
}
logger.info("Unlock lock with token " + lockToken);
clearLock(column);
}
private void appendToken(String token, long lockDuration, byte[] qualifier) throws IOException {
HBaseManager.act(getConnection(), tableName, table -> {
Append a = new Append(getRow());
byte[] columnFamily = getColumnFamily();
a.add(columnFamily, qualifier,
Bytes.toBytes(
token
+ LOCK_EXPIRING_DATE_SEPARATOR
+ (System.currentTimeMillis() + lockDuration)
+ LOCK_SEPARATOR));
table.append(a);
});
}
private void putCurrentLock(String token, long lockDuration, byte[] qualifier) throws IOException {
HBaseManager.act(getConnection(), tableName, table -> {
Put p = new Put(getRow());
byte[] columnFamily = getColumnFamily();
p.addColumn(columnFamily, qualifier,
Bytes.toBytes(
CURRENT_LOCK
+ token
+ LOCK_EXPIRING_DATE_SEPARATOR
+ (System.currentTimeMillis() + lockDuration)
+ LOCK_SEPARATOR));
table.put(p);
});
}
public void clearLock(byte[] qualifier) throws IOException {
HBaseManager.act(getConnection(), tableName, table -> {
Put p = new Put(getRow());
byte[] columnFamily = getColumnFamily();
p.addColumn(columnFamily, qualifier, Bytes.toBytes(""));
table.put(p);
});
}
/**
* A lock is taken if there is any lockValue in the array, and
* the token has not expired.
*
*
* @param lockValue
* @return
*/
private boolean isLockTaken(String[] lockValue) {
for (String lock : lockValue) {
if (lock.startsWith(CURRENT_LOCK)) {
return !isLockExpired(lock);
}
}
return false;
}
private boolean isLockExpired(String lock) {
String[] split = lock.split(LOCK_EXPIRING_DATE_SEPARATOR);
long expireDate = Long.parseLong(split[1]);
return expireDate < System.currentTimeMillis();
}
private String[] readLockValue(byte[] qualifier) throws IOException {
String lockValue;
lockValue = HBaseManager.act(getConnection(), tableName, table -> {
byte[] columnFamily = getColumnFamily();
Result result = table.get(new Get(getRow()).addColumn(columnFamily, qualifier));
if (result.isEmpty()) {
return null;
} else {
return Bytes.toString(result.getValue(columnFamily, qualifier));
}
});
if (lockValue == null || lockValue.isEmpty()) {
return new String[0];
} else {
return lockValue.split(LOCK_SEPARATOR);
}
}
private byte[] getRow() {
return studiesRow;
}
private byte[] getColumnFamily() {
return columnFamily;
}
public Connection getConnection() {
return hbaseManager.getConnection();
}
}