com.tacitknowledge.util.migration.jdbc.JdbcMigrationLauncher Maven / Gradle / Ivy
Show all versions of autopatch Show documentation
/* 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
*
* 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 java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.tacitknowledge.util.migration.MigrationContext;
import com.tacitknowledge.util.migration.MigrationException;
import com.tacitknowledge.util.migration.MigrationProcess;
import com.tacitknowledge.util.migration.MigrationTask;
import com.tacitknowledge.util.migration.PatchInfoStore;
import com.tacitknowledge.util.migration.RollbackListener;
import com.tacitknowledge.util.migration.RollbackableMigrationTask;
import com.tacitknowledge.util.migration.jdbc.loader.FlatXmlDataSetTaskSource;
import com.tacitknowledge.util.migration.jdbc.util.SqlUtil;
/**
* 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();
/** Create a new MigrationProcess and add a SqlScriptMigrationTaskSource */
public JdbcMigrationLauncher()
{
setMigrationProcess(getNewMigrationProcess());
// Make sure this class is notified when a patch is applied so that
// the patch level can be updated (see #migrationSuccessful).
migrationProcess.addListener(this);
getMigrationProcess().addMigrationTaskSource(new SqlScriptMigrationTaskSource());
getMigrationProcess().addMigrationTaskSource(new FlatXmlDataSetTaskSource());
}
/**
* 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()
{
return new 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");
}
Connection conn = null;
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);
}
finally
{
SqlUtil.close(conn, null, null);
}
}
/**
* 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
{
int patchLevel = patchTable.getPatchLevel();
// 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(patchLevel, 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;
Connection conn = null;
try
{
for (Iterator contextIter = contexts.keySet().iterator(); contextIter.hasNext();)
{
JdbcMigrationContext context = (JdbcMigrationContext) contextIter.next();
rollbackCount = doRollbacks(context, rollbackLevel);
log.info("Executed " + rollbackCount + " patches for context " + context);
}
}
catch (SQLException se)
{
throw new MigrationException("SqlException during rollback", se);
}
finally
{
SqlUtil.close(conn, null, null);
}
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;
Connection conn = null;
try
{
for (Iterator contextIter = contexts.keySet().iterator(); contextIter.hasNext();)
{
JdbcMigrationContext context = (JdbcMigrationContext) contextIter.next();
rollbackCount = doRollbacks(context, rollbackLevel, forceRollback);
log.info("Executed " + rollbackCount + " patches for context " + context);
}
}
catch (SQLException se)
{
throw new MigrationException("SqlException during rollback", se);
}
finally
{
SqlUtil.close(conn, null, null);
}
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)
{
migrationProcess.addPatchResourceDirectory(path);
}
else
{
migrationProcess.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();
int storePatchLevel = store.getPatchLevel();
if (patchLevel > storePatchLevel)
{
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
* @exception 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
* @exception 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
{
int patchLevel = patchTable.getPatchLevel();
// 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(patchLevel,
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)
{
// 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
{
PatchInfoStore piStore = new PatchTable(context);
// Make sure the table is created before claiming it exists by returning
piStore = (PatchInfoStore) contexts.get(context);
piStore.getPatchLevel();
return piStore;
}
/**
* 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()
{
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;
}
/**
* 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.updatePatchLevel(rollbackLevel);
}
}
}