package com.zendesk.maxwell.bootstrap;
import com.zendesk.maxwell.util.Logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import snaq.db.ConnectionPool;
import java.io.Console;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class MaxwellBootstrapUtility {
static final Logger LOGGER = LoggerFactory.getLogger(MaxwellBootstrapUtility.class);
protected class MissingBootstrapRowException extends Exception {
MissingBootstrapRowException(Long rowID) { super("Could not find bootstrap row with id: " + rowID); }
}
private static final long UPDATE_PERIOD_MILLIS = 250;
private static final long DISPLAY_PROGRESS_WARMUP_MILLIS = 5000;
private static final long NON_CONSOLE_DISPLAY_LINE_COUNT_MULTIPLE = 100000;
private static Console console = System.console();
private boolean isComplete = false;
private void run(String[] argv) throws Exception {
MaxwellBootstrapUtilityConfig config = new MaxwellBootstrapUtilityConfig(argv);
if ( config.log_level != null ) {
Logging.setLevel(config.log_level);
}
ConnectionPool connectionPool = getConnectionPool(config);
try ( final Connection connection = connectionPool.getConnection() ) {
if ( config.abortBootstrapID != null ) {
getInsertedRowsCount(connection, config.abortBootstrapID);
removeBootstrapRow(connection, config.abortBootstrapID);
return;
}
long rowId;
if ( config.monitorBootstrapID != null ) {
getInsertedRowsCount(connection, config.monitorBootstrapID);
rowId = config.monitorBootstrapID;
} else {
Long totalRows = calculateRowCount(connection, config.databaseName, config.tableName, config.whereClause);
rowId = insertBootstrapStartRow(connection, config.databaseName, config.tableName, config.whereClause, totalRows);
}
try {
monitorProgress(connection, rowId);
} catch ( MissingBootstrapRowException e ) {
LOGGER.error("bootstrap aborted.");
Runtime.getRuntime().halt(1);
}
} catch ( SQLException e ) {
LOGGER.error("failed to connect to mysql server @ " + config.getConnectionURI());
LOGGER.error(e.getLocalizedMessage());
e.printStackTrace();
System.exit(1);
}
}
private void monitorProgress(Connection connection, Long rowId) throws SQLException, MissingBootstrapRowException {
addMonitorShutdownHook(rowId);
long rowCount = getTotalRowCount(connection, rowId);
long initialRowCount, insertedRowsCount;
initialRowCount = getInsertedRowsCount(connection, rowId);
Long startedTimeMillis = null;
insertedRowsCount = initialRowCount;
while ( !isComplete ) {
if ( insertedRowsCount < rowCount ) {
if ( startedTimeMillis == null && insertedRowsCount > 0 ) {
startedTimeMillis = System.currentTimeMillis();
}
insertedRowsCount = getInsertedRowsCount(connection, rowId);
}
isComplete = getIsComplete(connection, rowId);
displayProgress(rowCount, insertedRowsCount, initialRowCount, startedTimeMillis);
try {
Thread.sleep(UPDATE_PERIOD_MILLIS);
} catch ( InterruptedException e) {}
}
displayLine("");
}
private void addMonitorShutdownHook(final Long rowId) {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
if ( !isComplete && console != null ) {
System.out.println("");
System.out.println("Exiting monitor. Bootstrap will continue in the background.");
System.out.println("To abort, run maxwell-bootstrap --abort " + rowId);
System.out.println("To resume monitoring, run maxwell-bootstrap --monitor " + rowId);
}
}
});
}
private long getInsertedRowsCount(Connection connection, long rowId) throws SQLException, MissingBootstrapRowException {
String sql = "select inserted_rows from `bootstrap` where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, rowId);
ResultSet resultSet = preparedStatement.executeQuery();
if ( resultSet.next() ) {
return resultSet.getLong(1);
} else {
throw new MissingBootstrapRowException(rowId);
}
}
private boolean getIsComplete(Connection connection, long rowId) throws SQLException, MissingBootstrapRowException {
String sql = "select is_complete from `bootstrap` where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, rowId);
ResultSet resultSet = preparedStatement.executeQuery();
if ( resultSet.next() ) {
return resultSet.getInt(1) == 1;
} else {
throw new MissingBootstrapRowException(rowId);
}
}
private ConnectionPool getConnectionPool(MaxwellBootstrapUtilityConfig config) {
String name = "MaxwellBootstrapConnectionPool";
int maxPool = 10;
int maxSize = 0;
int idleTimeout = 10;
String connectionURI = config.getConnectionURI();
String mysqlUser = config.mysqlUser;
String mysqlPassword = config.mysqlPassword;
return new ConnectionPool(name, maxPool, maxSize, idleTimeout, connectionURI, mysqlUser, mysqlPassword);
}
private Long getTotalRowCount(Connection connection, Long bootstrapRowID) throws SQLException, MissingBootstrapRowException {
ResultSet resultSet =
connection.createStatement().executeQuery("select total_rows from `bootstrap` where id = " + bootstrapRowID);
if ( resultSet.next() ) {
return resultSet.getLong(1);
} else {
throw new MissingBootstrapRowException(bootstrapRowID);
}
}
private Long calculateRowCount(Connection connection, String db, String table, String whereClause) throws SQLException {
LOGGER.info("counting rows");
String sql = String.format("select count(*) from `%s`.%s", db, table);
if ( whereClause != null ) {
sql += String.format(" where %s", whereClause);
}
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
resultSet.next();
return resultSet.getLong(1);
}
private long insertBootstrapStartRow(Connection connection, String db, String table, String whereClause, Long totalRows) throws SQLException {
LOGGER.info("inserting bootstrap start row");
String sql = null;
sql = "insert into `bootstrap` (database_name, table_name, where_clause, total_rows) values(?, ?, ?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, db);
preparedStatement.setString(2, table);
preparedStatement.setString(3, whereClause);
preparedStatement.setLong(4, totalRows);
preparedStatement.execute();
ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
generatedKeys.next();
return generatedKeys.getLong(1);
}
private void removeBootstrapRow(Connection connection, long rowId) throws SQLException {
LOGGER.info("deleting bootstrap start row");
String sql = "delete from `bootstrap` where id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, rowId);
preparedStatement.execute();
}
private void displayProgress(long total, long count, long initialCount, Long startedTimeMillis) {
if ( startedTimeMillis == null ) {
displayLine("waiting for bootstrap to start... ");
}
else if ( count < total) {
long currentTimeMillis = System.currentTimeMillis();
long elapsedMillis = currentTimeMillis - startedTimeMillis;
long predictedTotalMillis = ( long ) ((elapsedMillis / ( float ) (count - initialCount)) * (total - initialCount));
long remainingMillis = predictedTotalMillis - elapsedMillis;
String duration = prettyDuration(remainingMillis, elapsedMillis);
displayLine(String.format("%d / %d (%.2f%%) %s", count, total, ( count * 100.0 ) / total, duration), count);
} else {
displayLine("waiting for bootstrap to stop... ");
}
}
private String prettyDuration(long millis, long elapsedMillis) {
if ( elapsedMillis < DISPLAY_PROGRESS_WARMUP_MILLIS ) {
return "";
}
long d = (millis / (1000 * 60 * 60 * 24));
long h = (millis / (1000 * 60 * 60)) % 24;
long m = (millis / (1000 * 60)) % 60;
long s = (millis / (1000)) % 60;
if ( d > 0 ) {
return String.format("- %d days %02dh %02dm %02ds remaining ", d, h, m, s);
} else if ( h > 0 ) {
return String.format("- %02dh %02dm %02ds remaining ", h, m, s);
} else if ( m > 0 ) {
return String.format("- %02dm %02ds remaining ", m, s);
} else if ( s > 0 ) {
return String.format("- %02ds remaining ", s);
} else {
return "";
}
}
private void displayLine(String line, long count) {
if ( console == null && count > 0 && count % NON_CONSOLE_DISPLAY_LINE_COUNT_MULTIPLE == 0 ) {
System.out.println(line);
} else {
displayLine(line);
}
}
private void displayLine(String line) {
if ( console != null ) {
String ansiClearLine = "\u001b[2K";
String ansiMoveCursorToColumnZero = "\u001b[G";
System.out.print(ansiClearLine + ansiMoveCursorToColumnZero + line);
System.out.flush();
}
}
public static void main(String[] args) {
try {
new MaxwellBootstrapUtility().run(args);
} catch ( Exception e ) {
e.printStackTrace();
System.exit(1);
} finally {
LOGGER.info("done.");
}
}
}