liquibase.lockservice.StandardLockService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-core Show documentation
Show all versions of liquibase-core Show documentation
Liquibase is a tool for managing and executing database changes.
The newest version!
package liquibase.lockservice;
import liquibase.Scope;
import liquibase.change.Change;
import liquibase.changelog.ChangeLogHistoryService;
import liquibase.changelog.ChangeLogHistoryServiceFactory;
import liquibase.GlobalConfiguration;
import liquibase.database.Database;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.core.DB2Database;
import liquibase.database.core.DerbyDatabase;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.diff.output.DiffOutputControl;
import liquibase.diff.output.changelog.ChangeGeneratorFactory;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.LockException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.executor.LoggingExecutor;
import liquibase.executor.jvm.ChangelogJdbcMdcListener;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcObject;
import liquibase.logging.mdc.MdcValue;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.sql.Sql;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.*;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Table;
import liquibase.statement.core.RawParameterizedSqlStatement;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.logging.Level;
import static java.util.ResourceBundle.getBundle;
public class StandardLockService implements LockService {
protected static final ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");
protected Database database;
protected boolean hasChangeLogLock;
protected Long changeLogLockPollRate;
protected Long changeLogLockRecheckTime;
protected Boolean hasDatabaseChangeLogLockTable;
protected boolean isDatabaseChangeLogLockTableInitialized;
protected ObjectQuotingStrategy quotingStrategy;
protected final SecureRandom random = new SecureRandom();
public StandardLockService() {
//Empty constructor
}
@Override
public int getPriority() {
return PRIORITY_DEFAULT;
}
@Override
public boolean supports(Database database) {
return true;
}
@Override
public void setDatabase(Database database) {
this.database = database;
}
protected Long getChangeLogLockWaitTime() {
if (changeLogLockPollRate != null) {
return changeLogLockPollRate;
}
return GlobalConfiguration.CHANGELOGLOCK_WAIT_TIME.getCurrentValue();
}
@Override
public void setChangeLogLockWaitTime(long changeLogLockWaitTime) {
this.changeLogLockPollRate = changeLogLockWaitTime;
}
protected Long getChangeLogLockRecheckTime() {
if (changeLogLockRecheckTime != null) {
return changeLogLockRecheckTime;
}
return GlobalConfiguration.CHANGELOGLOCK_POLL_RATE.getCurrentValue();
}
@Override
public void setChangeLogLockRecheckTime(long changeLogLockRecheckTime) {
this.changeLogLockRecheckTime = changeLogLockRecheckTime;
}
@Override
public void init() throws DatabaseException {
boolean createdTable = false;
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
int maxIterations = 10;
if (executor instanceof LoggingExecutor) {
//can't / don't have to re-check
if (isDatabaseChangeLogLockTableCreated()) {
maxIterations = 0;
} else {
maxIterations = 1;
}
}
for (int i = 0; i < maxIterations; i++) {
try {
if (!isDatabaseChangeLogLockTableCreated(true)) {
executor.comment("Create Database Lock Table");
SqlStatement createLockTableStatement = new CreateDatabaseChangeLogLockTableStatement();
ChangelogJdbcMdcListener.execute(database, ex -> ex.execute(createLockTableStatement));
database.commit();
Scope.getCurrentScope().getLog(getClass()).fine(
"Created database lock table with name: " +
database.escapeTableName(
database.getLiquibaseCatalogName(),
database.getLiquibaseSchemaName(),
database.getDatabaseChangeLogLockTableName()
)
);
this.hasDatabaseChangeLogLockTable = true;
createdTable = true;
hasDatabaseChangeLogLockTable = true;
}
if (!isDatabaseChangeLogLockTableInitialized(createdTable, true)) {
executor.comment("Initialize Database Lock Table");
SqlStatement initializeLockTableStatement = new InitializeDatabaseChangeLogLockTableStatement();
ChangelogJdbcMdcListener.execute(database, ex -> ex.execute(initializeLockTableStatement));
database.commit();
}
if(!(executor instanceof LoggingExecutor)) {
handleOldChangelogTableFormat(executor);
}
break;
} catch (Exception e) {
if (i == maxIterations - 1) {
throw e;
} else {
Scope.getCurrentScope().getLog(getClass()).fine("Failed to create or initialize the lock table, trying again, iteration " + (i + 1) + " of " + maxIterations, e);
// If another node already created the table, then we need to rollback this current transaction,
// otherwise servers like Postgres will not allow continued use of the same connection, failing with
// a message like "current transaction is aborted, commands ignored until end of transaction block"
database.rollback();
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException ex) {
Scope.getCurrentScope().getLog(getClass()).warning("Lock table retry loop thread sleep interrupted", ex);
Thread.currentThread().interrupt();
}
}
}
}
}
private void handleOldChangelogTableFormat(Executor executor) throws DatabaseException {
if (executor.updatesDatabase() && (database instanceof DerbyDatabase) && ((DerbyDatabase) database)
.supportsBooleanDataType() || DB2Database.class.isAssignableFrom( database.getClass() ) && ((DB2Database) database)
.supportsBooleanDataType()) {
//check if the changelog table is of an old smallint vs. boolean format
String lockTable = database.escapeTableName(
database.getLiquibaseCatalogName(),
database.getLiquibaseSchemaName(),
database.getDatabaseChangeLogLockTableName()
);
Object obj = executor.queryForObject(
new RawParameterizedSqlStatement(
String.format("SELECT MIN(locked) AS test FROM %s FETCH FIRST ROW ONLY", lockTable)
), Object.class
);
if (!(obj instanceof Boolean)) { //wrong type, need to recreate table
executor.execute(
new DropTableStatement(
database.getLiquibaseCatalogName(),
database.getLiquibaseSchemaName(),
database.getDatabaseChangeLogLockTableName(),
false
)
);
executor.execute(new CreateDatabaseChangeLogLockTableStatement());
executor.execute(new InitializeDatabaseChangeLogLockTableStatement());
}
}
}
public boolean isDatabaseChangeLogLockTableInitialized(final boolean tableJustCreated) {
return isDatabaseChangeLogLockTableInitialized(tableJustCreated, false);
}
/**
* Determine whether the databasechangeloglock table has been initialized.
* @param forceRecheck if true, do not use any cached information, and recheck the actual database
*/
protected boolean isDatabaseChangeLogLockTableInitialized(final boolean tableJustCreated, final boolean forceRecheck) {
if (!isDatabaseChangeLogLockTableInitialized || forceRecheck) {
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
try {
SqlStatement lockTableInitializedStatement = new RawParameterizedSqlStatement(String.format("SELECT COUNT(*) FROM %s",
database.escapeTableName(
database.getLiquibaseCatalogName(),
database.getLiquibaseSchemaName(),
database.getDatabaseChangeLogLockTableName()
)
));
isDatabaseChangeLogLockTableInitialized = ChangelogJdbcMdcListener.query(database, ex -> ex.queryForInt(lockTableInitializedStatement)) > 0;
} catch (LiquibaseException e) {
if (executor.updatesDatabase()) {
throw new UnexpectedLiquibaseException(e);
} else {
//probably didn't actually create the table yet.
isDatabaseChangeLogLockTableInitialized = !tableJustCreated;
}
}
}
return isDatabaseChangeLogLockTableInitialized;
}
@Override
public boolean hasChangeLogLock() {
return hasChangeLogLock;
}
/**
* Check whether the databasechangeloglock table exists in the database.
* @param forceRecheck if true, do not use any cached information and check the actual database
*/
protected boolean isDatabaseChangeLogLockTableCreated(boolean forceRecheck) {
if (forceRecheck || hasDatabaseChangeLogLockTable == null) {
try {
hasDatabaseChangeLogLockTable = SnapshotGeneratorFactory.getInstance()
.hasDatabaseChangeLogLockTable(database);
} catch (LiquibaseException e) {
throw new UnexpectedLiquibaseException(e);
}
}
return hasDatabaseChangeLogLockTable;
}
protected boolean isDatabaseChangeLogLockTableCreated() throws DatabaseException {
return isDatabaseChangeLogLockTableCreated(false);
}
@Override
public void waitForLock() throws LockException {
boolean locked = false;
long timeToGiveUp = new Date().getTime() + (getChangeLogLockWaitTime() * 1000 * 60);
locked = acquireLock();
do {
if (!locked) {
Scope.getCurrentScope().getLog(getClass()).info("Waiting for changelog lock....");
try {
Thread.sleep(getChangeLogLockRecheckTime() * 1000);
} catch (InterruptedException e) {
// Restore thread interrupt status
Thread.currentThread().interrupt();
}
}
locked = acquireLock();
} while (!locked && (new Date().getTime() < timeToGiveUp));
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);
}
}
@Override
public boolean acquireLock() throws LockException {
if (hasChangeLogLock) {
return true;
}
quotingStrategy = database.getObjectQuotingStrategy();
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
try {
database.rollback();
this.init();
SqlStatement lockedStatement = new SelectFromDatabaseChangeLogLockStatement("LOCKED");
Boolean locked = ChangelogJdbcMdcListener.query(database, ex -> ex.queryForObject(lockedStatement, Boolean.class));
if (locked) {
return false;
} else {
executor.comment("Lock Database");
SqlStatement lockDatabaseStatement = new LockDatabaseChangeLogStatement();
int rowsUpdated = ChangelogJdbcMdcListener.query(database, ex -> ex.update(lockDatabaseStatement));
if ((rowsUpdated == -1) && (database instanceof MSSQLDatabase)) {
Scope.getCurrentScope().getLog(getClass()).fine(
"Database did not return a proper row count (Might have NOCOUNT enabled)"
);
database.rollback();
Sql[] sql = SqlGeneratorFactory.getInstance().generateSql(
new LockDatabaseChangeLogStatement(), database
);
if (sql.length != 1) {
throw new UnexpectedLiquibaseException("Did not expect "+sql.length+" statements");
}
SqlStatement noCountStatement = new RawParameterizedSqlStatement(String.format("EXEC sp_executesql N'SET NOCOUNT OFF %s'",
sql[0].toSql().replace("'", "''")));
rowsUpdated = ChangelogJdbcMdcListener.query(database, ex -> ex.update(noCountStatement));
}
if (rowsUpdated > 1) {
throw new LockException("Did not update change log lock correctly");
}
if (rowsUpdated == 0)
{
// another node was faster
return false;
}
database.commit();
Scope.getCurrentScope().getLog(getClass()).info(coreBundle.getString("successfully.acquired.change.log.lock"));
hasChangeLogLock = true;
Scope.getCurrentScope().getSingleton(ChangeLogHistoryServiceFactory.class).resetAll();
database.setCanCacheLiquibaseTableInfo(true);
return true;
}
} catch (Exception e) {
throw new LockException(e);
} finally {
try {
database.rollback();
} catch (DatabaseException e) {
Scope.getCurrentScope().getLog(getClass()).warning("Failed to rollback", e);
}
}
}
@Override
public void releaseLock() throws LockException {
ObjectQuotingStrategy incomingQuotingStrategy = null;
if (this.quotingStrategy != null) {
incomingQuotingStrategy = database.getObjectQuotingStrategy();
database.setObjectQuotingStrategy(this.quotingStrategy);
}
boolean success = false;
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
try {
if (this.isDatabaseChangeLogLockTableCreated()) {
executor.comment("Release Database Lock");
database.rollback();
SqlStatement unlockStatement = new UnlockDatabaseChangeLogStatement();
int updatedRows = ChangelogJdbcMdcListener.query(database, ex -> ex.update(unlockStatement));
if ((updatedRows == 0) && (database instanceof MySQLDatabase)) {
Scope.getCurrentScope().getLog(getClass()).fine(
"Database did not return a proper row count (Might have useAffectedRows enabled.)"
);
// NOTE: if using useAffectedRows, MySQL will return 0 rows affected if the changelog lock was not set or already released
if (((MySQLDatabase) database).getUseAffectedRows()) {
// Assume the lock was released successfully
updatedRows = 1;
}
}
if ((updatedRows == -1) && (database instanceof MSSQLDatabase)) {
Scope.getCurrentScope().getLog(getClass()).fine(
"Database did not return a proper row count (Might have NOCOUNT enabled.)"
);
database.rollback();
Sql[] sql = SqlGeneratorFactory.getInstance().generateSql(
new UnlockDatabaseChangeLogStatement(), database
);
if (sql.length != 1) {
throw new UnexpectedLiquibaseException("Did not expect "+sql.length+" statements");
}
SqlStatement noCountStatement = new RawParameterizedSqlStatement(
String.format("EXEC sp_executesql N'SET NOCOUNT OFF %s'",
sql[0].toSql().replace("'", "''")
)
);
updatedRows = ChangelogJdbcMdcListener.query(database, ex -> ex.update(noCountStatement));
}
if (updatedRows != 1) {
SqlStatement countStatement = new RawParameterizedSqlStatement(
String.format("SELECT COUNT(*) FROM %s",
database.getDatabaseChangeLogLockTableName())
);
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 " + ChangelogJdbcMdcListener.query(database, ex -> ex.queryForList(countStatement)) +
" rows in the table"
);
}
database.commit();
success = true;
}
} catch (Exception e) {
throw new LockException(e);
} finally {
try {
hasChangeLogLock = false;
database.setCanCacheLiquibaseTableInfo(false);
try (MdcObject releaseLocksOutcome = Scope.getCurrentScope().addMdcValue(MdcKey.RELEASE_LOCKS_OUTCOME, success ? MdcValue.COMMAND_SUCCESSFUL : MdcValue.COMMAND_FAILED)) {
Scope.getCurrentScope().getLog(getClass()).log(success ? Level.INFO : Level.WARNING, (success ? "Successfully released" : "Failed to release") + " change log lock", null);
}
database.rollback();
} catch (DatabaseException e) {
Scope.getCurrentScope().getLog(getClass()).warning("Failed to rollback", e);
}
if (incomingQuotingStrategy != null) {
database.setObjectQuotingStrategy(incomingQuotingStrategy);
}
}
}
@Override
public DatabaseChangeLogLock[] listLocks() throws LockException {
try {
if (!this.isDatabaseChangeLogLockTableCreated()) {
return new DatabaseChangeLogLock[0];
}
List allLocks = new ArrayList<>();
SqlStatement sqlStatement = new SelectFromDatabaseChangeLogLockStatement(
"ID", "LOCKED", "LOCKGRANTED", "LOCKEDBY"
);
List