com.tacitknowledge.util.migration.DistributedMigrationProcess Maven / Gradle / Ivy
/* 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;
import com.tacitknowledge.util.migration.jdbc.JdbcMigrationContext;
import com.tacitknowledge.util.migration.jdbc.JdbcMigrationLauncher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.*;
import java.util.Map.Entry;
/**
* Discovers and executes a sequence of system patches from multiple controlled
* systems, each of which has its own MigrationProcess.
*
* @author Mike Hardy ([email protected])
* @author Artie Pesh-Imam ([email protected])
* @author Hemri Herrera ([email protected])
* @author Ulises Pulido ([email protected])
* @see com.tacitknowledge.util.migration.MigrationProcess
*/
public class DistributedMigrationProcess extends MigrationProcess
{
/**
* Class logger
*/
private static Log log = LogFactory.getLog(DistributedMigrationProcess.class);
/**
* The JdbcMigrationLaunchers we are controlling, keyed by system name
*/
private HashMap controlledSystems = new HashMap();
/**
* If true, any nodes of the controlled systems that are not at the system's
* current patch level are patched to bring them in sync with the other
* nodes. This is not enabled by default because in a distributed system, if
* there are cross schema dependencies, then the patching of the node may
* fail since it is being patched 'out of order'.
*
* For example:
*
* system 1 has one node system 2 has one node
*
* patch1 applies to system1 and creates a table patch2 applies to system2
* and creates a table that references the table in system1 patch3 applies
* to system2, dropping the reference to the table in system1 patch4 applies
* to system1, dropping the table.
*
* Later, to add capacity, system2 has a node added. When the second node is
* forcibly 'synced' patches 2 and 3 are applied to it. The patching fails
* when patch2 is applied because the table no longer exists in system1.
*
* Therefore, forcing a sync is usually safe for systems that don't contain
* external references, but should not be used for interdependent systems.
*
* Instead, it would be better to import the schema from a node already at
* the current patch level using database tools, then the new node can
* participate in the regular patching process.
*/
private boolean forceSync = false;
/**
* Creates a new Migration instance.
*/
public DistributedMigrationProcess()
{
super();
}
/**
* Execute a dry run of the rollback process and return a count of the
* number of tasks which will rollback.
*
* @param rollbacks a List of RollbackableMigrationTasks
* @param rollbacksWithLaunchers a LinkedHashMap of task to launcher
* @return count of the number of rollbacks
*/
protected final int rollbackDryRun(final List rollbacks,
final LinkedHashMap rollbacksWithLaunchers)
{
// take the list of rollbacks
// iterate through the rollbacks
// log the context in which each rollback will execute
int taskCount = 0;
if (isPatchSetRollbackable(rollbacks))
{
for (Iterator i = rollbacks.iterator(); i.hasNext();)
{
RollbackableMigrationTask task = (RollbackableMigrationTask) i.next();
log.info("Will execute rollback for task '" + task.getName() + "'");
taskCount++;
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) rollbacksWithLaunchers
.get(task);
for (Iterator contextIterator = launcher.getContexts().keySet().iterator(); contextIterator
.hasNext();)
{
MigrationContext context = (MigrationContext) contextIterator.next();
log.debug("Task will execute in context '" + context + "'");
}
}
}
return taskCount;
}
/**
* Execute a dry run of the patch process and return a count of the number
* of patches we would have executed.
*
* @param currentPatchInfoStore The current patch info store
* @param migrationsWithLaunchers a map of migration task to launcher
* @return count of the number of patches
*/
protected final int patchDryRun(final PatchInfoStore currentPatchInfoStore,
final LinkedHashMap migrationsWithLaunchers) throws MigrationException
{
int taskCount = 0;
for (Iterator i = migrationsWithLaunchers.entrySet().iterator(); i.hasNext();)
{
Entry entry = (Entry) i.next();
MigrationTask task = (MigrationTask) entry.getKey();
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) entry.getValue();
if (getMigrationRunnerStrategy().shouldMigrationRun(task.getLevel(), currentPatchInfoStore))
{
log.info("Will execute patch task '" + getTaskLabel(task) + "'");
if (log.isDebugEnabled())
{
// Get all the contexts the task will execute in
for (Iterator j = launcher.getContexts().keySet().iterator(); j.hasNext();)
{
MigrationContext launcherContext = (MigrationContext) j.next();
log.debug("Task will execute in context '" + launcherContext + "'");
}
}
taskCount++;
}
}
return taskCount;
}
/**
* Applies the necessary rollbacks to the system.
*
* @param currentPatchInfoStore
* @param rollbackLevels the level that the system should rollback to
* @param context information and resources that are available to the migration tasks
* @return the number of RollbackableMigrationTasks which have been rolled back
* @throws MigrationException if a rollback fails
* @Override
*/
public final int doRollbacks(final PatchInfoStore currentPatchInfoStore, final int[] rollbackLevels, final MigrationContext context,
boolean forceRollback) throws MigrationException
{
log.debug("Starting doRollbacks");
// get all of the allTasks, with launchers, then get the list of just
// allTasks
LinkedHashMap rollbacksWithLaunchers = getMigrationTasksWithLaunchers();
List allTasks = new ArrayList();
allTasks.addAll(rollbacksWithLaunchers.keySet());
List rollbackCandidates = getMigrationRunnerStrategy().getRollbackCandidates(allTasks, rollbackLevels, currentPatchInfoStore);
validateControlledSystems(currentPatchInfoStore);
rollbackDryRun(rollbackCandidates, rollbacksWithLaunchers);
if (rollbackCandidates.size() > 0)
{
log.info("A total of " + rollbackCandidates.size() + " rollback patch tasks will execute.");
}
else
{
log.info("System up-to-date. No patch tasks will rollback.");
}
if (isPatchSetRollbackable(rollbackCandidates) || forceRollback)
{
if (isReadOnly())
{
throw new MigrationException("Unapplied rollbacks exist, but read-only flag is set");
}
for (Iterator rollbackIterator = rollbackCandidates.iterator(); rollbackIterator.hasNext();)
{
RollbackableMigrationTask task = (RollbackableMigrationTask) rollbackIterator
.next();
// Execute the task in the context it was loaded from
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) rollbacksWithLaunchers
.get(task);
// iterate through all the contexts
for (Iterator j = launcher.getContexts().keySet().iterator(); j.hasNext();)
{
MigrationContext launcherContext = (MigrationContext) j.next();
applyRollback(launcherContext, task, true);
}
}
}
else
{
log
.info("Could not complete rollback because one or more of the tasks is not rollbackable.");
}
List rollbacksNotApplied = getMigrationRunnerStrategy().getRollbackCandidates(rollbackCandidates,
rollbackLevels, currentPatchInfoStore);
if (rollbacksNotApplied.isEmpty())
{
log.info("Rollback complete (" + rollbackCandidates.size() + " patch tasks rolledback)");
}
else
{
log.info("The system could not rollback the tasks");
}
return rollbackCandidates.size() - rollbacksNotApplied.size();
}
/**
* Applies necessary patches to the system.
*
* @param patchInfoStore of the system to run
* @param context information and resources that are available to the migration tasks
* @return the number of MigrationTasks that have executed
* @throws MigrationException if a migration fails
* @Override
*/
public final int doMigrations(final PatchInfoStore patchInfoStore,
final MigrationContext context) throws MigrationException
{
log.debug("Starting doMigrations");
// Get all the migrations, with their launchers, then get the list of
// just the migrations
LinkedHashMap migrationsWithLaunchers = getMigrationTasksWithLaunchers();
List migrations = new ArrayList();
migrations.addAll(migrationsWithLaunchers.keySet());
// make sure the migrations are okay, then sort them
validateTasks(migrations);
Collections.sort(migrations);
validateControlledSystems(patchInfoStore);
// determine how many tasks we're going to execute
int taskCount = patchDryRun(patchInfoStore, migrationsWithLaunchers);
if (taskCount > 0)
{
log.info("A total of " + taskCount + " patch tasks will execute.");
}
else
{
log.info("System up-to-date. No patch tasks will execute.");
}
// See if we should execute
if (isReadOnly())
{
if (taskCount > 0)
{
throw new MigrationException("Unapplied patches exist, but read-only flag is set");
}
log.info("In read-only mode - skipping patch application");
return 0;
}
// Roll through each migration, applying it if necessary
taskCount = 0;
for (Iterator i = migrations.iterator(); i.hasNext();)
{
MigrationTask task = (MigrationTask) i.next();
int migrationLevel = task.getLevel().intValue();
boolean shouldApplyPatch = getMigrationRunnerStrategy().shouldMigrationRun(migrationLevel, patchInfoStore);
if (shouldApplyPatch && !forceSync)
{
// Execute the task in the context it was loaded from
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) migrationsWithLaunchers
.get(task);
// Get all the contexts the task will execute in
for (Iterator j = launcher.getContexts().keySet().iterator(); j.hasNext();)
{
MigrationContext launcherContext = (MigrationContext) j.next();
applyPatch(launcherContext, task, true);
}
taskCount++;
}
else if (forceSync)// if a sync is forced, need to check all
// the contexts to identify the ones out of
// sync
{
boolean patchesApplied = false;
ArrayList outOfSyncContexts = new ArrayList();
// first need to iterate over all the contexts and determined
// which one's are out of sync.
// can't sync yet because if there are multiple contexts that
// are out of sync, after the
// first one is synced, the remaining one's have their patch
// level updated via the
// MigrationListener.migrationSuccessful event.
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) migrationsWithLaunchers
.get(task);
for (Iterator j = launcher.getContexts().keySet().iterator(); j.hasNext();)
{
MigrationContext launcherContext = (MigrationContext) j.next();
PatchInfoStore patchInfoStoreOfContext =
(PatchInfoStore) launcher.getContexts().get(
launcherContext);
if (!getMigrationRunnerStrategy().isSynchronized(patchInfoStore, patchInfoStoreOfContext))
{
outOfSyncContexts.add(launcherContext);
}
}
// next patch the contexts that have been determined to be out
// of sync
for (Iterator iter = outOfSyncContexts.iterator(); iter.hasNext();)
{
MigrationContext launcherContext = (MigrationContext) iter.next();
applyPatch(launcherContext, task, true);
patchesApplied = true;
}
if (patchesApplied)
{
taskCount++;
}
} // else if forceSync
}
if (taskCount > 0)
{
log.info("Patching complete (" + taskCount + " patch tasks executed)");
}
else
{
log.info("System up-to-date. No patch tasks have been run.");
}
return taskCount;
}
/**
* Validates that the controlled systems are all at the current patch level.
*
* @param currentPatchInfoStore
* @throws MigrationException if all the controlled systems are not at the current patch level.
*/
protected final void validateControlledSystems(final PatchInfoStore currentPatchInfoStore) throws MigrationException
{
for (Iterator it = getControlledSystems().keySet().iterator(); it.hasNext();)
{
String systemName = (String) it.next();
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) getControlledSystems().get(
systemName);
for (Iterator contextIt = launcher.getContexts().keySet().iterator(); contextIt
.hasNext();)
{
JdbcMigrationContext ctx = (JdbcMigrationContext) contextIt.next();
PatchInfoStore patchInfoStore = (PatchInfoStore) launcher.getContexts().get(ctx);
if (!getMigrationRunnerStrategy().isSynchronized(currentPatchInfoStore, patchInfoStore))
{
String message = "Database " + ctx.getDatabaseName()
+ " is out of sync with system: " + systemName + ". "
+ ctx.getDatabaseName() + " is at patch level "
+ Integer.toString(patchInfoStore.getPatchLevel()) + " and the System is at patch level "
+ Integer.toString(currentPatchInfoStore.getPatchLevel()) + ".";
if (getForceSync())
{
log.info(message + " Continuing since 'forcesync' was specified.");
}
else
{
throw new MigrationException(message);
}
}
}
}
}
/**
* Returns a LinkedHashMap of task/launcher pairings, regardless of patch
* level.
*
* @return LinkedHashMap containing MigrationTask / JdbcMigrationLauncher
* pairings
* @throws MigrationException if one or more migration tasks could not be created
*/
public final LinkedHashMap getMigrationTasksWithLaunchers() throws MigrationException
{
LinkedHashMap tasks = new LinkedHashMap();
// Roll through all our controlled system names
for (Iterator controlledSystemIter = getControlledSystems().keySet().iterator(); controlledSystemIter
.hasNext();)
{
// Get the sub launcher that runs patches for the current name
String controlledSystemName = (String) controlledSystemIter.next();
JdbcMigrationLauncher subLauncher = (JdbcMigrationLauncher) getControlledSystems().get(
controlledSystemName);
// Get all the tasks for that sub launcher
List subTasks = subLauncher.getMigrationProcess().getMigrationTasks();
log.info("Found " + subTasks.size() + " for system " + controlledSystemName);
for (Iterator subTaskIter = subTasks.iterator(); subTaskIter.hasNext();)
{
MigrationTask task = (MigrationTask) subTaskIter.next();
if (log.isDebugEnabled())
{
Iterator launchers = subLauncher.getContexts().keySet().iterator();
String systemName = ((JdbcMigrationContext) launchers.next()).getSystemName();
log.debug("\tMigration+Launcher binder found subtask " + task.getName()
+ " for launcher context " + systemName);
}
// store the task, related to its launcher
tasks.put(task, subLauncher);
}
}
return tasks;
}
/**
* Returns a List of MigrationTasks, regardless of patch level.
*
* @return List containing MigrationTask objects
* @throws MigrationException if one or more migration tasks could not be created
*/
public final List getMigrationTasks() throws MigrationException
{
List tasks = new ArrayList();
for (Iterator controlledSystemIter = getControlledSystems().keySet().iterator(); controlledSystemIter
.hasNext();)
{
String controlledSystemName = (String) controlledSystemIter.next();
JdbcMigrationLauncher launcher = (JdbcMigrationLauncher) getControlledSystems().get(
controlledSystemName);
List subTasks = launcher.getMigrationProcess().getMigrationTasks();
log.info("Found " + subTasks.size() + " for system " + controlledSystemName);
if (log.isDebugEnabled())
{
for (Iterator subTaskIter = subTasks.iterator(); subTaskIter.hasNext();)
{
log.debug("\tFound subtask " + ((MigrationTask) subTaskIter.next()).getName());
}
}
tasks.addAll(subTasks);
}
// Its difficult to tell what's going on when you don't see any patches.
// This will help people realize they don't have patches, and perhaps
// help them discover why.
if (tasks.size() == 0)
{
log.info("No patch tasks were discovered in your classpath. "
+ "Run with DEBUG logging enabled for patch search details.");
}
return tasks;
}
/**
* Get the list of systems we are controlling
*
* @return HashMap of JdbcMigrationLauncher objects keyed by String system
* names
*/
public final HashMap getControlledSystems()
{
return controlledSystems;
}
/**
* Set the list of systems to control
*
* @param controlledSystems HashMap of system name / JdbcMigrationLauncher pairs
*/
public final void setControlledSystems(final HashMap controlledSystems)
{
this.controlledSystems = controlledSystems;
}
public final boolean getForceSync()
{
return forceSync;
}
public final void setForceSync(final boolean forceSync)
{
this.forceSync = forceSync;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy