/*
* 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 liquibase.lockservice;
import static org.ngrinder.common.util.NoOp.noOp;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.exception.LockException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogFactory;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.LockExDatabaseChangeLogStatement;
import liquibase.statement.core.RawSqlStatement;
import liquibase.statement.core.SelectFromDatabaseChangeLogLockStatement;
import liquibase.statement.core.UnlockDatabaseChangeLogStatement;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extended {@link LockService} to use 'T' or 'F' for the lock table's boolean
* column.
*
* @author JunHo Yoon
* @since 3.0
*/
public final class LockServiceEx {
private final Logger LOGGER = LoggerFactory.getLogger(LockServiceEx.class);
private Database database;
private boolean hasChangeLogLock = false;
private long changeLogLockWaitTime = 1000 * 60 * 5; // default to 5 mins
private long changeLogLocRecheckTime = 1000 * 10; // default to every 10
// seconds
private static Map<Database, LockServiceEx> instances = new ConcurrentHashMap<Database, LockServiceEx>();
private LockServiceEx(Database database) {
this.database = database;
}
/**
* Get {@link LockServiceEx} instance.
*
* @param database
* corresponding database instance
* @return {@link LockServiceEx} instance
*/
public static LockServiceEx getInstance(Database database) {
if (!instances.containsKey(database)) {
instances.put(database, new LockServiceEx(database));
}
return instances.get(database);
}
public void setChangeLogLockWaitTime(long changeLogLockWaitTime) {
this.changeLogLockWaitTime = changeLogLockWaitTime;
}
public void setChangeLogLockRecheckTime(long changeLogLocRecheckTime) {
this.changeLogLocRecheckTime = changeLogLocRecheckTime;
}
/**
* Check if it has change log lock.
*
* @return true if it has the change lock
*/
public boolean hasChangeLogLock() {
return hasChangeLogLock;
}
/**
* Wait for lock.
*
* @throws LockException
* occurs when lock manipulation is failed.
*/
public void waitForLock() throws LockException {
boolean locked = false;
long timeToGiveUp = new Date().getTime() + changeLogLockWaitTime;
while (!locked && new Date().getTime() < timeToGiveUp) {
locked = acquireLock();
if (!locked) {
LOGGER.info("Waiting for changelog lock....");
try {
Thread.sleep(changeLogLocRecheckTime);
} catch (InterruptedException e) {
noOp();
}
}
}
if (!locked) {
DatabaseChangeLogLock[] locks = listLocks();
String lockedBy;
if (locks.length > 0) {
DatabaseChangeLogLock lock = locks[0];
lockedBy = lock.getLockedBy()
+ " since "
+ DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(
lock.getLockGranted());
} else {
lockedBy = "UNKNOWN";
}
throw new LockException("Could not acquire change log lock. Currently locked by " + lockedBy);
}
}
/**
* Acquire lock. Instead of liquibase implementation, nGrinder added the
* type resolution for boolean value.
*
* @return true if successful
* @throws LockException
* occurs when the lock aquire is failed.
*/
public boolean acquireLock() throws LockException {
if (hasChangeLogLock) {
return true;
}
Executor executor = ExecutorService.getInstance().getExecutor(database);
try {
database.rollback();
database.checkDatabaseChangeLogLockTable();
Object lockObject = (Object) ExecutorService.getInstance().getExecutor(database)
.queryForObject(new SelectFromDatabaseChangeLogLockStatement("LOCKED"), Object.class);
if (checkReturnValue(lockObject)) {
// To here
return false;
} else {
executor.comment("Lock Database");
int rowsUpdated = executor.update(new LockExDatabaseChangeLogStatement());
if (rowsUpdated > 1) {
throw new LockException("Did not update change log lock correctly");
}
if (rowsUpdated == 0) {
// another node was faster
return false;
}
database.commit();
LOGGER.info("Successfully acquired change log lock");
hasChangeLogLock = true;
database.setCanCacheLiquibaseTableInfo(true);
return true;
}
} catch (Exception e) {
throw new LockException(e);
} finally {
try {
database.rollback();
} catch (DatabaseException e) {
noOp();
}
}
}
/**
* Check return value is boolean or not.
*
* @param value
* returnValue
* @return true if true
*/
public boolean checkReturnValue(Object value) {
if (value instanceof String) {
String trim = StringUtils.trim((String) value);
if ("T".equals(trim)) {
return true;
} else if ("F".equals(trim) || StringUtils.isEmpty((String) value) || "0".equals(trim)) {
return false;
} else {
throw new UnexpectedLiquibaseException("Unknown boolean value: " + value);
}
} else if (value == null) {
return false;
} else if (value instanceof Integer) {
return !(Integer.valueOf(0).equals(value));
} else if (value instanceof Long) {
return !(Long.valueOf(0).equals(value));
} else if (value instanceof Boolean) {
return ((Boolean) value);
} else {
return false;
}
}
/**
* Release Lock.
*
* @throws LockException
* exception.
*/
public void releaseLock() throws LockException {
Executor executor = ExecutorService.getInstance().getExecutor(database);
try {
if (database.hasDatabaseChangeLogLockTable()) {
executor.comment("Release Database Lock");
database.rollback();
int updatedRows = executor.update(new UnlockDatabaseChangeLogStatement());
if (updatedRows != 1) {
throw new LockException("Did not update change log lock correctly.\n\n"
+ updatedRows
+ " rows were updated instead of the expected 1 row using executor "
+ executor.getClass().getName()
+ " there are "
+ executor.queryForInt(new RawSqlStatement("select count(*) from "
+ database.getDatabaseChangeLogLockTableName())) + " rows in the table");
}
database.commit();
hasChangeLogLock = false;
instances.remove(this.database);
database.setCanCacheLiquibaseTableInfo(false);
LOGGER.info("Successfully released change log lock");
}
} catch (Exception e) {
throw new LockException(e);
} finally {
try {
database.rollback();
} catch (DatabaseException e) {
noOp();
}
}
}
/**
* List up locks.
*
* @return {@link DatabaseChangeLogLock} array.
* @throws LockException
* occurs when lock list up is failed.
*/
@SuppressWarnings("rawtypes")
public DatabaseChangeLogLock[] listLocks() throws LockException {
try {
if (!database.hasDatabaseChangeLogLockTable()) {
return new DatabaseChangeLogLock[0];
}
List<DatabaseChangeLogLock> allLocks = new ArrayList<DatabaseChangeLogLock>();
SqlStatement sqlStatement = new SelectFromDatabaseChangeLogLockStatement("ID", "LOCKED", "LOCKGRANTED",
"LOCKEDBY");
List<Map> rows = ExecutorService.getInstance().getExecutor(database).queryForList(sqlStatement);
for (Map columnMap : rows) {
Object lockedValue = columnMap.get("LOCKED");
Boolean locked;
if (lockedValue instanceof Number) {
locked = ((Number) lockedValue).intValue() == 1;
} else if (lockedValue instanceof String) {
locked = ("T".equals(lockedValue));
} else {
locked = (Boolean) lockedValue;
}
if (locked != null && locked) {
allLocks.add(new DatabaseChangeLogLock(((Number) columnMap.get("ID")).intValue(), (Date) columnMap
.get("LOCKGRANTED"), (String) columnMap.get("LOCKEDBY")));
}
}
return allLocks.toArray(new DatabaseChangeLogLock[allLocks.size()]);
} catch (Exception e) {
throw new LockException(e);
}
}
/**
* Releases whatever locks are on the database change log table.
*
* @throws LockException
* exception
* @throws DatabaseException
* exception
*/
public void forceReleaseLock() throws LockException, DatabaseException {
database.checkDatabaseChangeLogLockTable();
releaseLock();
/*
* try { releaseLock(); } catch (LockException e) { // ignore ?
* LogFactory.getLogger().info("Ignored exception in forceReleaseLock: "
* + e.getMessage()); }
*/
}
/**
* Clears information the lock handler knows about the tables. Should only
* be called by Liquibase internal calls
*/
public void reset() {
hasChangeLogLock = false;
}
/**
* Reset all locks.
*/
public static void resetAll() {
for (Map.Entry<Database, LockServiceEx> entity : instances.entrySet()) {
entity.getValue().reset();
}
}
}