All Downloads are FREE. Search and download functionalities are using the official Maven repository.

liquibase.ext.TimeoutLockService Maven / Gradle / Ivy

The newest version!
package liquibase.ext;

import liquibase.GlobalConfiguration;
import liquibase.Scope;
import liquibase.database.Database;
import liquibase.datatype.DataTypeFactory;
import liquibase.exception.DatabaseException;
import liquibase.exception.LockException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.lockservice.DatabaseChangeLogLock;
import liquibase.lockservice.StandardLockService;
import liquibase.logging.Logger;
import liquibase.statement.core.UpdateStatement;

import java.sql.Timestamp;
import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

/**
 * Will force release any locks older than the configured changeLogLockWaitTimeInMinutes to avoid crash loops due to
 * unclean shutdowns.
 */
public class TimeoutLockService extends StandardLockService {

    private static final Logger log = Scope.getCurrentScope().getLog(TimeoutLockService.class);
    public static final int DEFAULT_LOCK_RENEWAL_INTERVAL = 60000;
    private static final AtomicBoolean SHOULD_RENEW_LOCK = new AtomicBoolean(false);

    private final long timeoutMilliseconds;
    private Thread lockRenewalThread;
    private final long lockRenewalInterval;

    private static Supplier createRenewalConnectionCreator;

    public TimeoutLockService() {
        this(DEFAULT_LOCK_RENEWAL_INTERVAL);
    }

    public TimeoutLockService(long lockRenewalInterval) {
        this.lockRenewalInterval = lockRenewalInterval;
        timeoutMilliseconds = GlobalConfiguration.CHANGELOGLOCK_WAIT_TIME.getCurrentValue() * 60 * 1000;
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (hasChangeLogLock()) {
                try {
                    log.warning("Releasing liquibase lock due to shutdown");
                    releaseLock();
                } catch (LockException e) {
                    log.warning("Failed to release lock", e);
                }
            }
        }));
    }

    @Override
    public void waitForLock() throws LockException {
        if (this.acquireLock()) {
            return;
        }

        releaseOldLock();

        try {
            super.waitForLock();
        } catch (LockException lockException) {
            releaseOldLock();
            if (!this.acquireLock()) {
                throw lockException;
            }
        }
    }

    private void releaseOldLock() throws LockException {
        Date timeout = new Date(System.currentTimeMillis() - timeoutMilliseconds);
        for (DatabaseChangeLogLock lock : this.listLocks()) {
            if (lock.getLockGranted().before(timeout)) {
                try {
                    String lockedBy = lock.getLockedBy() + " since " + DateFormat.getDateTimeInstance(3, 3).format(lock.getLockGranted());
                    log.warning("Forcing release of lock held by: " + lockedBy);
                    this.forceReleaseLock();
                    break;
                } catch (DatabaseException e) {
                    throw new LockException(e);
                }
            }
        }
    }

    @Override
    public int getPriority() {
        return super.getPriority() + 1;
    }

    @Override
    public boolean acquireLock() throws LockException {
        boolean acquiredLock = super.acquireLock();
        if (acquiredLock) {
            ensureLockRenewalRunning();
        }
        return acquiredLock;
    }

    @Override
    public synchronized void releaseLock() throws LockException {
        super.releaseLock();
        SHOULD_RENEW_LOCK.set(false);
        if (lockRenewalThread != null) {
            lockRenewalThread.interrupt();
        }
    }

    private void ensureLockRenewalRunning() {
        if (SHOULD_RENEW_LOCK.compareAndSet(false, true)) {
            lockRenewalThread = new Thread(() -> {
                while (SHOULD_RENEW_LOCK.get()) {
                    try {
                        Thread.sleep(lockRenewalInterval);
                        renewLock();
                    } catch (InterruptedException e) {
                        // Normal
                    } catch (Exception e) {
                        log.severe("Failed renewing lock", e);
                    }
                }
            });

            lockRenewalThread.start();
        }
    }

    private synchronized void renewLock() throws DatabaseException {
        if (SHOULD_RENEW_LOCK.get() && hasChangeLogLock()) {
            Database renewalDatabase = createRenewalConnectionCreator.get();
            try {
                ExecutorService executorService = Scope.getCurrentScope().getSingleton(ExecutorService.class);
                Executor executor = executorService.getExecutor("jdbc", renewalDatabase);

                String liquibaseSchema = database.getLiquibaseSchemaName();
                String liquibaseCatalog = database.getLiquibaseCatalogName();

                // Copied from https://github.com/liquibase/liquibase/blob/master/liquibase-core/src/main/java/liquibase/sqlgenerator/core/LockDatabaseChangeLogGenerator.java#L42
                UpdateStatement updateStatement = new UpdateStatement(liquibaseCatalog, liquibaseSchema, database.getDatabaseChangeLogLockTableName());
                updateStatement.addNewColumnValue("LOCKGRANTED", new Timestamp(new java.util.Date().getTime()));
                updateStatement.setWhereClause(database
                    .escapeColumnName(liquibaseCatalog, liquibaseSchema, database.getDatabaseChangeLogTableName(), "ID")
                    + " = 1 AND "
                    + this.database.escapeColumnName(liquibaseCatalog, liquibaseSchema, this.database.getDatabaseChangeLogTableName(), "LOCKED")
                    + " = "
                    + DataTypeFactory.getInstance().fromDescription("boolean", database).objectToSql(true, database)
                );

                int changedRows = executor.update(updateStatement);
                log.info("Renewing liquibase lock, locks updated: " + changedRows);
            } finally {
                renewalDatabase.close();
            }
        }
    }

    public static void setRenewalConnectionCreator(Supplier createRenewalConnectionCreator) {
        TimeoutLockService.createRenewalConnectionCreator = createRenewalConnectionCreator;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy