com.tacitknowledge.util.migration.jdbc.JdbcMigrationLauncher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of autopatch Show documentation
Show all versions of autopatch Show documentation
An automated Java patching system
The newest version!
/* Copyright 2004 Tacit Knowledge
*
* 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 get
*
* 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 com.tacitknowledge.util.migration.jdbc;
import com.tacitknowledge.util.migration.*;
import com.tacitknowledge.util.migration.jdbc.loader.FlatXmlDataSetTaskSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
/**
* Core starting point for a database migration run. This class obtains a
* connection to the database, checks its patch level, delegates the actual
* execution of the migration tasks to a MigrationProcess
* instance, and then commits and cleans everything up at the end.
*
* This class is NOT threadsafe.
*
* @author Scott Askew ([email protected])
*/
public class JdbcMigrationLauncher implements RollbackListener
{
/**
* Class logger
*/
private static Log log = LogFactory.getLog(JdbcMigrationLauncher.class);
/**
* The MigrationProcess responsible for applying the patches
*/
private MigrationProcess migrationProcess = null;
/**
* The amount time, in milliseconds, between attempts to obtain a lock on
* the patches table. Defaults to 15 seconds.
*/
private long lockPollMillis = 15000;
/**
* The number of times to wait for the lock before overriding it. -1 is
* infinite
*/
private int lockPollRetries = -1;
/**
* The path containing directories and packages to search through to locate
* patches.
*/
private String patchPath = null;
/**
* The path containing directories and packages to search for post-patch
* tasks.
*/
private String postPatchPath = null;
/**
* The MigrationContext objects to use for all migrations.
* Each one is the key in the map, with the PatchInfoStore being the value
*/
private LinkedHashMap contexts = new LinkedHashMap();
/**
* Holds the migration strategy to use during migration process
*/
private String migrationStrategy;
/**
* Create a new MigrationProcess and add a SqlScriptMigrationTaskSource
*/
public JdbcMigrationLauncher()
{
}
/**
* Create a new MigrationLancher.
*
* @param context the JdbcMigrationContext to use.
*/
public JdbcMigrationLauncher(JdbcMigrationContext context)
{
this();
addContext(context);
}
/**
* Get the MigrationProcess we'll use to migrate things
*
* @return MigrationProcess for migration control
*/
public MigrationProcess getNewMigrationProcess()
{
MigrationProcess migrationProcess = new MigrationProcess();
migrationProcess.setMigrationRunnerStrategy
(MigrationRunnerFactory.getMigrationRunnerStrategy(getMigrationStrategy()));
return migrationProcess;
}
/**
* Starts the application migration process.
*
* @return the number of patches applied
* @throws MigrationException if an unrecoverable error occurs during the migration
*/
public int doMigrations() throws MigrationException
{
if (contexts.size() == 0)
{
throw new MigrationException("You must configure a migration context");
}
try
{
Iterator contextIter = contexts.keySet().iterator();
int migrationCount = 0;
while (contextIter.hasNext())
{
JdbcMigrationContext context =
(JdbcMigrationContext) contextIter.next();
migrationCount = doMigrations(context);
log.info("Executed " + migrationCount + " patches for context "
+ context);
}
return migrationCount;
}
catch (SQLException e)
{
throw new MigrationException("SqlException during migration", e);
}
}
/**
* Performs the application rollbacks
*
* @param context the database context to run the patches in
* @param rollbackLevel the level the system should rollback to
* @return the number of patches applied
* @throws SQLException if an unrecoverable database error occurs while working with the patches table.
* @throws MigrationException if an unrecoverable error occurs during the migration
*/
public int doRollbacks(JdbcMigrationContext context, int[] rollbackLevel)
throws SQLException, MigrationException
{
return doRollbacks(context, rollbackLevel, false);
}
/**
* Performs the application rollbacks
*
* @param context the database context to run the patches in
* @param rollbackLevel the level the system should rollback to
* @param forceRollback is a boolean indicating if the application should ignore a check to see if all patches are rollbackable
* @return the number of patches applied
* @throws SQLException if an unrecoverable database error occurs while working with the patches table.
* @throws MigrationException if an unrecoverable error occurs during the migration
*/
public int doRollbacks(JdbcMigrationContext context, int[] rollbackLevel, boolean forceRollback)
throws SQLException, MigrationException
{
PatchInfoStore patchTable = createPatchStore(context);
lockPatchStore(context);
// Now apply the patches
int executedPatchCount = 0;
try
{
// remember the auto-commit state, and turn auto-commit off
Connection conn = context.getConnection();
boolean commitState = conn.getAutoCommit();
conn.setAutoCommit(false);
// run the rollbacks
try
{
executedPatchCount = migrationProcess.doRollbacks(patchTable, rollbackLevel, context, forceRollback);
}
// restore autocommit state
finally
{
if ((conn != null) && !conn.isClosed())
{
conn.setAutoCommit(commitState);
}
}
}
catch (MigrationException me)
{
// If there was any kind of error, we don't want to eat it, but we do
// want to unlock the patch store. So do that, then re-throw.
patchTable.unlockPatchStore();
throw me;
}
// Do any post-patch tasks
try
{
migrationProcess.doPostPatchMigrations(context);
return executedPatchCount;
}
finally
{
try
{
patchTable.unlockPatchStore();
}
catch (MigrationException e)
{
log.error("Error unlocking patch table: ", e);
}
}
}
/**
* Initiates the application rollback process.
*
* @param rollbackLevel the patch level the system should rollback to
* @return an integer indicating how many patches were rolled back
* @throws MigrationException is thrown in case of an error while rolling back
*/
public int doRollbacks(int[] rollbackLevel) throws MigrationException
{
if (contexts.size() == 0)
{
throw new MigrationException(
"You must configure a migration context");
}
int rollbackCount = 0;
try
{
for (JdbcMigrationContext context : contexts.keySet())
{
rollbackCount = doRollbacks(context, rollbackLevel);
log.info("Executed " + rollbackCount + " patches for context " + context);
}
}
catch (SQLException se)
{
throw new MigrationException("SqlException during rollback", se);
}
return rollbackCount;
}
/**
* Initiates the application rollback process.
*
* @param rollbackLevel the patch level the system should rollback to
* @param forceRollback a boolean indcating if the check for all tasks being rollbackable should be ignored
* @return an integer indicating how many patches were rolled back
* @throws MigrationException is thrown in case of an error while rolling back
*/
public int doRollbacks(int[] rollbackLevel, boolean forceRollback) throws MigrationException
{
if (contexts.size() == 0)
{
throw new MigrationException(
"You must configure a migration context");
}
int rollbackCount = 0;
try
{
for (JdbcMigrationContext context : contexts.keySet())
{
rollbackCount = doRollbacks(context, rollbackLevel, forceRollback);
log.info("Executed " + rollbackCount + " patches for context " + context);
}
}
catch (SQLException se)
{
throw new MigrationException("SqlException during rollback", se);
}
return rollbackCount;
}
/**
* Returns the colon-separated path of packages and directories within the
* class path that are sources of patches.
*
* @return a colon-separated path of packages and directories within the
* class path that are sources of patches
*/
public String getPatchPath()
{
return patchPath;
}
/**
* Sets the colon-separated path of packages and directories within the
* class path that are sources of patches.
*
* @param searchPath a colon-separated path of packages and directories within
* the class path that are sources of patches
*/
public void setPatchPath(String searchPath)
{
this.patchPath = searchPath;
StringTokenizer st = new StringTokenizer(searchPath, ":");
while (st.hasMoreTokens())
{
String path = st.nextToken();
if (path.indexOf('/') > -1)
{
getMigrationProcess().addPatchResourceDirectory(path);
}
else
{
getMigrationProcess().addPatchResourcePackage(path);
}
}
}
/**
* Returns the colon-separated path of packages and directories within the
* class path that are sources of post-patch tasks
*
* @return a colon-separated path of packages and directories within the
* class path that are sources of post-patch tasks
*/
public String getPostPatchPath()
{
return postPatchPath;
}
/**
* Sets the colon-separated path of packages and directories within the
* class path that are sources of post-patch tasks
*
* @param searchPath a colon-separated path of packages and directories within
* the class path that are sources of post-patch tasks
*/
public void setPostPatchPath(String searchPath)
{
this.postPatchPath = searchPath;
if (searchPath == null)
{
return;
}
StringTokenizer st = new StringTokenizer(searchPath, ":");
while (st.hasMoreTokens())
{
String path = st.nextToken();
if (path.indexOf('/') > -1)
{
migrationProcess.addPostPatchResourceDirectory(path);
}
else
{
migrationProcess.addPostPatchResourcePackage(path);
}
}
}
/**
* {@inheritDoc}
*/
public void migrationStarted(MigrationTask task, MigrationContext ctx) throws MigrationException
{
log.debug("Started task " + task.getName() + " for context " + ctx);
}
/**
* {@inheritDoc}
*/
public void migrationSuccessful(MigrationTask task, MigrationContext ctx) throws MigrationException
{
log.debug("Task " + task.getName() + " was successful for context " + ctx + " in launcher " + this);
int patchLevel = task.getLevel().intValue();
// update all of our controlled patch tables
for (Iterator patchTableIter = contexts.entrySet().iterator(); patchTableIter.hasNext();)
{
PatchInfoStore store = (PatchInfoStore) ((Map.Entry) patchTableIter.next()).getValue();
MigrationRunnerStrategy strategy = getMigrationProcess().getMigrationRunnerStrategy();
if (strategy.shouldMigrationRun(patchLevel, store))
{
store.updatePatchLevel(patchLevel);
}
}
}
/**
* {@inheritDoc}
*/
public void migrationFailed(MigrationTask task, MigrationContext ctx, MigrationException e)
throws MigrationException
{
log.debug("Task " + task.getName() + " failed for context " + ctx, e);
}
/**
* Get the patch level from the database
*
* @param ctx the migration context to get the patch level for
* @return int representing the current database patch level
* @throws MigrationException if there is a database connection error, or the patch level can't be determined
*/
public int getDatabasePatchLevel(MigrationContext ctx) throws MigrationException
{
PatchInfoStore patchTable = (PatchInfoStore) contexts.get(ctx);
return patchTable.getPatchLevel();
}
/**
* Get the next patch level, for use when creating a new patch
*
* @return int representing the first unused patch number
* @throws MigrationException if the next patch level can't be determined
*/
public int getNextPatchLevel() throws MigrationException
{
return migrationProcess.getNextPatchLevel();
}
/**
* Sets the JdbcMigrationContext used for the migrations.
*
* @param context the JdbcMigrationContext used for the migrations
*/
public void addContext(JdbcMigrationContext context)
{
PatchInfoStore patchTable = new PatchTable(context);
log.debug("Adding context " + context + " with patch table " + patchTable + " in launcher " + this);
contexts.put(context, patchTable);
}
/**
* Returns the JdbcMigrationContext objects used for the migrations.
*
* @return Map of JdbcMigrationContext and
* PatchInfoStore objects used in the migrations
*/
public LinkedHashMap getContexts()
{
return contexts;
}
/**
* Performs the application migration process in one go
*
* @param context the database context to run the patches in
* @return the number of patches applied
* @throws SQLException if an unrecoverable database error occurs while working with the patches table.
* @throws MigrationException if an unrecoverable error occurs during the migration
*/
protected int doMigrations(JdbcMigrationContext context) throws SQLException, MigrationException
{
PatchInfoStore patchTable = createPatchStore(context);
lockPatchStore(context);
// Now apply the patches
int executedPatchCount = 0;
try
{
// remember the auto-commit state, and turn auto-commit off
Connection conn = context.getConnection();
boolean commitState = conn.getAutoCommit();
conn.setAutoCommit(false);
// run the migrations
try
{
executedPatchCount = migrationProcess.doMigrations(patchTable,
context);
}
// restore autocommit state
finally
{
if ((conn != null) && !conn.isClosed())
{
conn.setAutoCommit(commitState);
}
}
}
catch (MigrationException me)
{
// If there was any kind of error, we don't want to eat it, but we do
// want to unlock the patch store. So do that, then re-throw.
patchTable.unlockPatchStore();
throw me;
}
// Do any post-patch tasks
try
{
migrationProcess.doPostPatchMigrations(context);
return executedPatchCount;
}
finally
{
try
{
patchTable.unlockPatchStore();
}
catch (MigrationException e)
{
log.error("Error unlocking patch table: ", e);
}
}
}
/**
* Lock the patch store. This is done safely, such that we safely handle the
* case where other migration launchers are patching at the same time.
*
* @param context the context to lock the store in
* @throws MigrationException if the reading or setting lock state fails
*/
private void lockPatchStore(JdbcMigrationContext context) throws MigrationException
{
// Patch locks ensure that only one system sharing a patch store will patch
// it at the same time.
boolean lockObtained = false;
while (!lockObtained)
{
waitForFreeLock(context);
PatchInfoStore piStore = (PatchInfoStore) contexts.get(context);
piStore.getPatchLevel();
try
{
piStore.lockPatchStore();
lockObtained = true;
}
catch (IllegalStateException ise)
{
log.error("IllegalStateException when trying to lock the patch info store", ise);
// this happens when someone woke up at the same time,
// raced us to the lock and won. We re-sleep and try again.
}
}
}
/**
* create a patch table object for use in migrations
*
* @param context the context to create the store in
* @return PatchTable object for use in accessing patch state information
* @throws MigrationException if unable to create the store
*/
protected PatchInfoStore createPatchStore(JdbcMigrationContext context) throws MigrationException
{
// Make sure the table is created before claiming it exists by returning
return (PatchInfoStore) contexts.get(context);
}
/**
* Pauses until the patch lock become available.
*
* @param context the context related to the store
* @throws MigrationException if an unrecoverable error occurs
*/
private void waitForFreeLock(JdbcMigrationContext context) throws MigrationException
{
PatchInfoStore piStore = (PatchInfoStore) contexts.get(context);
log.debug("about to wait for free lock");
for (int i = 0; piStore.isPatchStoreLocked(); i++)
{
// Have we exceeded our threshold of time to wait?
if ((getLockPollRetries() != -1) && (i >= getLockPollRetries()))
{
log.info("Reached maximum lock poll retries (" + getLockPollRetries() + "), overriding patch lock");
piStore.unlockPatchStore();
}
else
{
log.info("Waiting for migration lock for system \"" + context.getSystemName() + "\"");
log.info(" If this isn't from a long-running patch, but a stale lock, either:");
log.info(" 1) run MigrationTableUnlock (probably 'ant patch.unlock')");
log.info(" 2) set the lockPollRetries property so the lock times out");
log.info(" (this is dangerous in combination with long-running patches)");
log.info(" 3) set the 'patch_in_progress' in the patches table to 'F'");
if (getLockPollRetries() != -1)
{
log.info("'lockPollRetries' is set, will poll lock " + (getLockPollRetries() - i)
+ " more times before overriding lock.");
}
try
{
Thread.sleep(getLockPollMillis());
}
catch (InterruptedException e)
{
log.error("Received InterruptedException while waiting for patch lock", e);
}
}
}
log.debug("done waiting for free lock");
}
/**
* Get how long to wait for the patch store lock
*
* @return the wait time for the patch store, in milliseconds
*/
public long getLockPollMillis()
{
return lockPollMillis;
}
/**
* Set how long to wait for the patch store lock
*
* @param lockPollMillis the wait time for the patch store, in milliseconds
*/
public void setLockPollMillis(long lockPollMillis)
{
this.lockPollMillis = lockPollMillis;
}
/**
* Get the migration process to use for migrations
*
* @return MigrationProcess to use for migrations
*/
public MigrationProcess getMigrationProcess()
{
if (migrationProcess == null)
{
setMigrationProcess(getNewMigrationProcess());
}
return migrationProcess;
}
/**
* Set the migration process to use for migrations
*
* @param migrationProcess the MigrationProcess to use for migrations
*/
public void setMigrationProcess(MigrationProcess migrationProcess)
{
this.migrationProcess = migrationProcess;
// Make sure this class is notified when a patch is applied so that
// the patch level can be updated (see #migrationSuccessful).
this.migrationProcess.addListener(this);
this.migrationProcess.addMigrationTaskSource(new SqlScriptMigrationTaskSource());
this.migrationProcess.addMigrationTaskSource(new FlatXmlDataSetTaskSource());
}
/**
* See if we are actually applying patches, or if it is just readonly
*
* @return boolean true if we will skip application
*/
public boolean isReadOnly()
{
return getMigrationProcess().isReadOnly();
}
/**
* Set whether or not to actually apply patches
*
* @param readOnly boolean true if we should skip application
*/
public void setReadOnly(boolean readOnly)
{
getMigrationProcess().setReadOnly(readOnly);
}
/**
* Return the number of times to poll the lock before overriding it. -1 is infinite
*
* @return int either -1 for infinite or number of times to poll before override
*/
public int getLockPollRetries()
{
return lockPollRetries;
}
/**
* Set the number of times to poll the lock before overriding it. -1 is infinite
*
* @param lockPollRetries either -1 for infinite or number of times to poll before override
*/
public void setLockPollRetries(int lockPollRetries)
{
this.lockPollRetries = lockPollRetries;
}
/**
* Explicitly set the contexts.
*
* @param contexts the collection of contexts that is a map of JDBCMigrationContext -> PatchInfoStore.
*/
public void setContexts(LinkedHashMap contexts)
{
this.contexts = contexts;
}
/**
* @see com.tacitknowledge.util.migration.MigrationListener#initialize(String, Properties)
*/
public void initialize(String systemName, Properties properties) throws MigrationException
{
}
/**
* {@inheritDoc}
*/
public void rollbackFailed(RollbackableMigrationTask task, MigrationContext context, MigrationException e)
throws MigrationException
{
log.debug("Task " + task.getName() + " failed for context " + context, e);
}
/**
* {@inheritDoc}
*/
public void rollbackStarted(RollbackableMigrationTask task, MigrationContext context) throws MigrationException
{
log.debug("Started rollback " + task.getName() + " for context " + context);
}
/**
* {@inheritDoc}
*/
public void rollbackSuccessful(RollbackableMigrationTask task, int rollbackLevel, MigrationContext context) throws MigrationException
{
log.debug("Rollback of task " + task.getName() + " was successful for context " + context + " in launcher " + this);
int patchLevel = task.getLevel().intValue();
// update all of our controlled patch tables
for (Iterator patchTableIter = contexts.entrySet().iterator(); patchTableIter.hasNext();)
{
PatchInfoStore store = (PatchInfoStore) ((Map.Entry) patchTableIter.next()).getValue();
store.updatePatchLevelAfterRollBack(rollbackLevel);
}
}
public void setMigrationStrategy(String migrationStrategy)
{
this.migrationStrategy = migrationStrategy;
}
public String getMigrationStrategy()
{
return migrationStrategy;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy