liquibase.lockservice.LockServiceEx.java Source code

Java tutorial

Introduction

Here is the source code for liquibase.lockservice.LockServiceEx.java

Source

/* 
 * 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();
        }
    }

}