Skip to content

Commit

Permalink
DAT-16798: Add new parameters to rollback report (#5630)
Browse files Browse the repository at this point in the history
* [DAT-16798] Add new parameters to rollback report. Simplify some database info setup with shared method.

* [DAT-16798] Include correct failed count.

* [DAT-16922] Rework ExceptionDetails to encapsulate building of error object. Update commandscope to reflect ExceptionDetails changes. Use lombok getters on DefaultChangeExecListener. Add pending rollbacks, exception details and failed changeset name to rollback parameters.

* [DAT-16922] Set size for pending rollbacks.

* [DAT-16922] Rework ExceptionDetails to encapsulate building of error object. Update commandscope to reflect ExceptionDetails changes. Use lombok getters on DefaultChangeExecListener. Add pending rollbacks, exception details and failed changeset name to rollback parameters.

* [DAT-16922] Set size for pending rollbacks.

* [DAT-16568] Cleanup update report parameters imports.

[DAT-17041] Add deployment id and targeted changeset details to rollback report parameters.

* [DAT-16798] Simplify setting rollback report parameters. Actually report error when no tag is found.

---------

Co-authored-by: obovsunivskyii <baqaua@gmail.com>
  • Loading branch information
abrackx and obovsunivskyii committed Mar 21, 2024
1 parent a9de072 commit 7b4ac21
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,23 @@
public class DefaultChangeExecListener implements ChangeExecListener, ChangeLogSyncListener {
@Getter
private final List<ChangeExecListener> listeners;
/**
* Get the list of ChangeSets that have been deployed during a given Liquibase command.
* For example: if you ran update with three ChangeSets in total and the third ChangeSet failed,
* this list will contain the first two ChangeSets that were executed without exception.
*
* @return the list of ChangeSets deployed during a command.
*/
@Getter
private final List<ChangeSet> deployedChangeSets = new LinkedList<>();
@Getter
private final List<ChangeSet> rolledBackChangeSets = new LinkedList<>();
/**
* Gets list of failed ChangeSets encountered during a given Liquibase command.
*
* @return the list of ChangeSets which have failed to deploy.
*/
@Getter
private final List<ChangeSet> failedChangeSets = new LinkedList<>();
@Getter
private final List<ChangeSet> failedRollbackChangeSets = new LinkedList<>();
Expand Down Expand Up @@ -90,26 +104,6 @@ public void rollbackFailed(ChangeSet changeSet, DatabaseChangeLog databaseChange
listeners.forEach(listener -> listener.rollbackFailed(changeSet, databaseChangeLog, database, exception));
}

/**
* Get the list of ChangeSets that have been deployed during a given Liquibase command.
* For example: if you ran update with three ChangeSets in total and the third ChangeSet failed,
* this list will contain the first two ChangeSets that were executed without exception.
*
* @return the list of ChangeSets deployed during a command.
*/
public List<ChangeSet> getDeployedChangeSets() {
return deployedChangeSets;
}

/**
* Gets list of failed ChangeSets encountered during a given Liquibase command.
*
* @return the list of ChangeSets which have failed to deploy.
*/
public List<ChangeSet> getFailedChangeSets() {
return failedChangeSets;
}

/**
* Gets list of Changes deployed during the current ChangeSet execution. This list is dynamic and will update depending on where in the lifecycle this is being called.
*
Expand All @@ -127,11 +121,6 @@ public void addListener(ChangeExecListener listener) {
}
}

/**
* @param changeSet
* @param databaseChangeLog
* @param database
*/
@Override
public void markedRan(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database) {
// no-op
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
import liquibase.database.Database;
import liquibase.exception.CommandExecutionException;
import liquibase.exception.CommandValidationException;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.integration.commandline.LiquibaseCommandLineConfiguration;
import liquibase.listener.LiquibaseListener;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcManager;
import liquibase.logging.mdc.MdcObject;
import liquibase.logging.mdc.MdcValue;
import liquibase.logging.mdc.customobjects.ExceptionDetails;
import liquibase.util.LiquibaseUtil;
import liquibase.util.StringUtil;
import lombok.Getter;

Expand Down Expand Up @@ -228,15 +225,14 @@ public CommandResults execute() throws CommandExecutionException {
}
executedCommands.add(command);
}
String source = null;

// To find the correct database source if there was an exception
// we need to examine the database connection prior to closing it.
// That means this must run prior to any cleanup command steps.
Database database = (Database) getDependency(Database.class);
String source = null;
if (database != null) {
try {
source = getDatabaseInfo(database);
} catch (Exception e) {
Scope.getCurrentScope().getLog(CommandScope.class).warning("Unable to obtain database info: " + e.getMessage());
source = database.getDisplayName();
}
source = ExceptionDetails.findSource(database);
}

// after executing our pipeline, runs cleanup in inverse order
Expand Down Expand Up @@ -276,46 +272,17 @@ public CommandResults execute() throws CommandExecutionException {
return resultsBuilder.build();
}

private static String getDatabaseInfo(Database database) {
String source = "Database";
try {
source = String.format("%s %s", database.getDatabaseProductName(), database.getDatabaseProductVersion());
} catch (DatabaseException dbe) {
source = database.getDatabaseProductName();
}
return source;
}

private void logPrimaryExceptionToMdc(Throwable exception, String source) {
//
// Drill down to get the lowest level exception
//
Throwable primaryException = exception;
while (primaryException != null && primaryException.getCause() != null) {
primaryException = primaryException.getCause();
}
if (primaryException != null) {
if (primaryException instanceof LiquibaseException || source == null) {
source = LiquibaseUtil.getBuildVersionInfo();
}
ExceptionDetails exceptionDetails = new ExceptionDetails();
String simpleName = primaryException.getClass().getSimpleName();
exceptionDetails.setPrimaryException(simpleName);
exceptionDetails.setPrimaryExceptionReason(primaryException.getMessage());
exceptionDetails.setPrimaryExceptionSource(source);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
exception.printStackTrace(pw);
String exceptionString = sw.toString();
exceptionDetails.setException(exceptionString);
ExceptionDetails exceptionDetails = new ExceptionDetails(exception, source);
if (exceptionDetails.getPrimaryException() != null) {
MdcManager mdcManager = Scope.getCurrentScope().getMdcManager();
try (MdcObject primaryExceptionObject = mdcManager.put(MdcKey.EXCEPTION_DETAILS, exceptionDetails)) {
Scope.getCurrentScope().getLog(getClass()).info("Logging exception.");
}
Scope.getCurrentScope().getUI().sendMessage("ERROR: Exception Details");
Scope.getCurrentScope().getUI().sendMessage("ERROR: Exception Primary Class: " + simpleName);
Scope.getCurrentScope().getUI().sendMessage("ERROR: Exception Primary Reason: " + primaryException.getMessage());
Scope.getCurrentScope().getUI().sendMessage("ERROR: Exception Primary Source: " + source);
Scope.getCurrentScope().getUI().sendMessage(exceptionDetails.getFormattedPrimaryException());
Scope.getCurrentScope().getUI().sendMessage(exceptionDetails.getFormattedPrimaryExceptionReason());
Scope.getCurrentScope().getUI().sendMessage(exceptionDetails.getFormattedPrimaryExceptionSource());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@
import liquibase.logging.mdc.MdcObject;
import liquibase.logging.mdc.MdcValue;
import liquibase.logging.mdc.customobjects.ChangesetsRolledback;
import liquibase.logging.mdc.customobjects.ExceptionDetails;
import liquibase.report.RollbackReportParameters;
import liquibase.resource.Resource;
import liquibase.util.StreamUtil;
import liquibase.util.StringUtil;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;

/**
* AbstractRollbackCommandStep provides the common operations to all the rollback* commands.
Expand Down Expand Up @@ -128,13 +127,30 @@ public static void doRollback(Database database, String changelogFile, String ro
rollbackReportParameters.getOperationInfo().setOperationOutcome(MdcValue.COMMAND_SUCCESSFUL);
}
}
} catch (Exception exception) {
if (rollbackReportParameters != null) {
rollbackReportParameters.setSuccess(false);
String source = ExceptionDetails.findSource(database);
ExceptionDetails exceptionDetails = new ExceptionDetails(exception, source);
rollbackReportParameters.setRollbackException(exceptionDetails);
// Rethrow the exception to be handled by child classes.
throw exception;
}
} finally {
if (rollbackReportParameters != null && changeExecListener instanceof DefaultChangeExecListener) {
List<ChangeSet> failedChangeSets = ((DefaultChangeExecListener) changeExecListener).getFailedRollbackChangeSets();
List<ChangeSet> rolledBackChangeSets = ((DefaultChangeExecListener) changeExecListener).getRolledBackChangeSets();
DefaultChangeExecListener defaultListener = (DefaultChangeExecListener) changeExecListener;
List<ChangeSet> failedChangeSets = defaultListener.getFailedRollbackChangeSets();
List<ChangeSet> rolledBackChangeSets = defaultListener.getRolledBackChangeSets();
List<ChangeSet> pendingChangeSets = logIterator.getSkippedDueToExceptionChangeSets();
Map<ChangeSet, String> pendingChangeSetMap = new HashMap<>();
pendingChangeSets.forEach(changeSet -> pendingChangeSetMap.put(changeSet, "Unexpected error running Liquibase."));
rollbackReportParameters.getChangesetInfo().setChangesetCount(failedChangeSets.size() + rolledBackChangeSets.size());
rollbackReportParameters.getChangesetInfo().addAllToChangesetInfoList(rolledBackChangeSets, true);
rollbackReportParameters.getChangesetInfo().addAllToChangesetInfoList(failedChangeSets, true);
rollbackReportParameters.getChangesetInfo().setFailedChangesetCount(failedChangeSets.size());
rollbackReportParameters.getChangesetInfo().addAllToPendingChangesetInfoList(pendingChangeSetMap);
rollbackReportParameters.getChangesetInfo().setPendingChangesetCount(pendingChangeSetMap.size());
rollbackReportParameters.setFailedChangeset(failedChangeSets.stream().map(ChangeSet::toString).collect(Collectors.joining(", ")));
}
}
}
Expand Down Expand Up @@ -243,6 +259,7 @@ private static void handleRollbackException(String operationName, RollbackReport
Scope.getCurrentScope().getLog(AbstractRollbackCommandStep.class).info(operationName + " command encountered an exception.");
if (rollbackReportParameters != null) {
rollbackReportParameters.getOperationInfo().setOperationOutcome(MdcValue.COMMAND_FAILED);
rollbackReportParameters.setSuccess(false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import liquibase.command.*;
import liquibase.database.Database;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.customobjects.ExceptionDetails;
import liquibase.report.RollbackReportParameters;
import liquibase.util.StringUtil;

Expand Down Expand Up @@ -51,13 +52,21 @@ public void run(CommandResultsBuilder resultsBuilder) throws Exception {
Scope.getCurrentScope().addMdcValue(MdcKey.ROLLBACK_TO_TAG, tagToRollBackTo);

Database database = (Database) commandScope.getDependency(Database.class);
rollbackReportParameters.getDatabaseInfo().setDatabaseType(database.getDatabaseProductName());
rollbackReportParameters.getDatabaseInfo().setVersion(database.getDatabaseProductVersion());
rollbackReportParameters.setJdbcUrl(database.getConnection().getURL());
rollbackReportParameters.setupDatabaseInfo(database);
rollbackReportParameters.setRollbackTag(tagToRollBackTo);

List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
TagVersionEnum tagVersion = TagVersionEnum.valueOf(commandScope.getArgumentValue(TAG_VERSION_ARG));
this.doRollback(resultsBuilder, ranChangeSetList, new AfterTagChangeSetFilter(tagToRollBackTo, ranChangeSetList, tagVersion), rollbackReportParameters);
try {
AfterTagChangeSetFilter afterTagChangeSetFilter = new AfterTagChangeSetFilter(tagToRollBackTo, ranChangeSetList, tagVersion); // This can throw an exception
this.doRollback(resultsBuilder, ranChangeSetList, afterTagChangeSetFilter, rollbackReportParameters);
} catch (Exception exception) {
rollbackReportParameters.setSuccess(false);
String source = ExceptionDetails.findSource(database);
ExceptionDetails exceptionDetails = new ExceptionDetails(exception, source);
rollbackReportParameters.setRollbackException(exceptionDetails);
throw exception;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ public void run(CommandResultsBuilder resultsBuilder) throws Exception {
resultsBuilder.addResult("rollbackReport", rollbackReportParameters);

Database database = (Database) commandScope.getDependency(Database.class);
rollbackReportParameters.getDatabaseInfo().setDatabaseType(database.getDatabaseProductName());
rollbackReportParameters.getDatabaseInfo().setVersion(database.getDatabaseProductVersion());
rollbackReportParameters.setJdbcUrl(database.getConnection().getURL());
rollbackReportParameters.setupDatabaseInfo(database);
rollbackReportParameters.setRollbackCount(changesToRollback);

List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public void run(CommandResultsBuilder resultsBuilder) throws Exception {
CommandScope commandScope = resultsBuilder.getCommandScope();
Date dateToRollBackTo = commandScope.getArgumentValue(DATE_ARG);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
Scope.getCurrentScope().addMdcValue(MdcKey.ROLLBACK_TO_DATE, formatter.format(dateToRollBackTo));
String stringDateToRollbackTo = formatter.format(dateToRollBackTo);
Scope.getCurrentScope().addMdcValue(MdcKey.ROLLBACK_TO_DATE, stringDateToRollbackTo);

RollbackReportParameters rollbackReportParameters = new RollbackReportParameters();
rollbackReportParameters.setCommandTitle(
Expand All @@ -43,9 +44,8 @@ public void run(CommandResultsBuilder resultsBuilder) throws Exception {
resultsBuilder.addResult("rollbackReport", rollbackReportParameters);

Database database = (Database) commandScope.getDependency(Database.class);
rollbackReportParameters.getDatabaseInfo().setDatabaseType(database.getDatabaseProductName());
rollbackReportParameters.getDatabaseInfo().setVersion(database.getDatabaseProductVersion());
rollbackReportParameters.setJdbcUrl(database.getConnection().getURL());
rollbackReportParameters.setupDatabaseInfo(database);
rollbackReportParameters.setRollbackDate(stringDateToRollbackTo);

List<RanChangeSet> ranChangeSetList = database.getRanChangeSetList();
Scope.child(Collections.singletonMap("rollbackReport", rollbackReportParameters), () -> this.doRollback(resultsBuilder, ranChangeSetList, new ExecutedAfterChangeSetFilter(dateToRollBackTo, ranChangeSetList), rollbackReportParameters));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package liquibase.logging.mdc.customobjects;

import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.logging.mdc.CustomMdcObject;
import liquibase.util.LiquibaseUtil;
import lombok.Getter;
import lombok.Setter;

import java.io.PrintWriter;
import java.io.StringWriter;

@Getter
@Setter
public class ExceptionDetails implements CustomMdcObject {
Expand All @@ -14,4 +21,61 @@ public class ExceptionDetails implements CustomMdcObject {

public ExceptionDetails() {
}

public ExceptionDetails(Throwable exception, String source) {
//
// Drill down to get the lowest level exception
//
Throwable primaryException = exception;
while (primaryException != null && primaryException.getCause() != null) {
primaryException = primaryException.getCause();
}
if (primaryException != null) {
if (primaryException instanceof LiquibaseException || source == null) {
source = LiquibaseUtil.getBuildVersionInfo();
}
this.primaryException = primaryException.getClass().getSimpleName();
this.primaryExceptionReason = primaryException.getMessage();
this.primaryExceptionSource = source;
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
exception.printStackTrace(printWriter);
this.exception = stringWriter.toString();
}
}

public String getFormattedPrimaryException() {
return getPrimaryException() != null
? String.format("ERROR: Exception Primary Class: %s", getPrimaryException())
: "";
}

public String getFormattedPrimaryExceptionReason() {
return getPrimaryExceptionReason() != null
? String.format("ERROR: Exception Primary Reason: %s", getPrimaryExceptionReason())
: "";
}

public String getFormattedPrimaryExceptionSource() {
return getPrimaryExceptionSource() != null
? String.format("ERROR: Exception Primary Source: %s", getPrimaryExceptionSource())
: "";
}

public static String findSource(Database database) {
try {
String source;
try {
source = String.format("%s %s", database.getDatabaseProductName(), database.getDatabaseProductVersion());
} catch (DatabaseException dbe) {
source = database.getDatabaseProductName();
}
return source;
} catch (RuntimeException ignored) {
// For some reason we decided to have AbstractJdbcDatabase#getDatabaseProductName throw a runtime exception.
// In this case since we always want to fall back to some sort of identifier for the database
// we can just ignore and return the display name.
return database.getDisplayName();
}
}
}

0 comments on commit 7b4ac21

Please sign in to comment.