/** * Database-independent synchronization strategy that does full record transfer between two * databases. This strategy is best used when there are <em>many</em> differences between the active * database and the inactive database (i.e. very much out of sync). The following algorithm is used: * * <ol> * <li>Drop the foreign keys on the inactive database (to avoid integrity constraint violations) * <li>For each database table: * <ol> * <li>Delete all rows in the inactive database table * <li>Query all rows on the active database table * <li>For each row in active database table: * <ol> * <li>Insert new row into inactive database table * </ol> * </ol> * <li>Re-create the foreign keys on the inactive database * <li>Synchronize sequences * </ol> * * @author Paul Ferraro */ public class FullSynchronizationStrategy implements SynchronizationStrategy, TableSynchronizationStrategy, Serializable { private static final long serialVersionUID = 9190347092842178162L; private static Logger logger = LoggerFactory.getLogger(FullSynchronizationStrategy.class); private SynchronizationStrategy strategy = new PerTableSynchronizationStrategy(this); private int maxBatchSize = 100; private int fetchSize = 0; @Override public String getId() { return "full"; } @Override public <Z, D extends Database<Z>> void init(DatabaseCluster<Z, D> cluster) { this.strategy.init(cluster); } @Override public <Z, D extends Database<Z>> void synchronize(SynchronizationContext<Z, D> context) throws SQLException { this.strategy.synchronize(context); } @Override public <Z, D extends Database<Z>> void destroy(DatabaseCluster<Z, D> cluster) { this.strategy.destroy(cluster); } @Override public <Z, D extends Database<Z>> void synchronize( SynchronizationContext<Z, D> context, TableProperties table) throws SQLException { final String tableName = table.getName().getDMLName(); final Collection<String> columns = table.getColumns(); final String commaDelimitedColumns = Strings.join(columns, Strings.PADDED_COMMA); final String selectSQL = String.format("SELECT %s FROM %s", commaDelimitedColumns, tableName); final String deleteSQL = context.getDialect().getTruncateTableSQL(table); final String insertSQL = String.format( "INSERT INTO %s (%s) VALUES (%s)", tableName, commaDelimitedColumns, Strings.join( Collections.nCopies(columns.size(), Strings.QUESTION), Strings.PADDED_COMMA)); Connection sourceConnection = context.getConnection(context.getSourceDatabase()); final Statement selectStatement = sourceConnection.createStatement(); try { selectStatement.setFetchSize(this.fetchSize); Callable<ResultSet> callable = new Callable<ResultSet>() { @Override public ResultSet call() throws SQLException { logger.log(Level.DEBUG, selectSQL); return selectStatement.executeQuery(selectSQL); } }; Future<ResultSet> future = context.getExecutor().submit(callable); Connection targetConnection = context.getConnection(context.getTargetDatabase()); Statement deleteStatement = targetConnection.createStatement(); try { logger.log(Level.DEBUG, deleteSQL); int deletedRows = deleteStatement.executeUpdate(deleteSQL); logger.log(Level.INFO, Messages.DELETE_COUNT.getMessage(), deletedRows, tableName); } finally { Resources.close(deleteStatement); } logger.log(Level.DEBUG, insertSQL); PreparedStatement insertStatement = targetConnection.prepareStatement(insertSQL); try { int statementCount = 0; ResultSet resultSet = future.get(); while (resultSet.next()) { int index = 0; for (String column : table.getColumns()) { index += 1; int type = context.getDialect().getColumnType(table.getColumnProperties(column)); Object object = context.getSynchronizationSupport().getObject(resultSet, index, type); if (resultSet.wasNull()) { insertStatement.setNull(index, type); } else { insertStatement.setObject(index, object, type); } } insertStatement.addBatch(); statementCount += 1; if ((statementCount % this.maxBatchSize) == 0) { insertStatement.executeBatch(); insertStatement.clearBatch(); } insertStatement.clearParameters(); } if ((statementCount % this.maxBatchSize) > 0) { insertStatement.executeBatch(); } logger.log(Level.INFO, Messages.INSERT_COUNT.getMessage(), statementCount, table); } catch (ExecutionException e) { throw ExceptionType.getExceptionFactory(SQLException.class).createException(e.getCause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(e); } finally { Resources.close(insertStatement); } } finally { Resources.close(selectStatement); } } @Override public <Z, D extends Database<Z>> void dropConstraints(SynchronizationContext<Z, D> context) throws SQLException { context.getSynchronizationSupport().dropForeignKeys(); } @Override public <Z, D extends Database<Z>> void restoreConstraints(SynchronizationContext<Z, D> context) throws SQLException { context.getSynchronizationSupport().restoreForeignKeys(); } /** @return the fetchSize. */ public int getFetchSize() { return this.fetchSize; } /** @param fetchSize the fetchSize to set. */ public void setFetchSize(int fetchSize) { this.fetchSize = fetchSize; } /** @return the maxBatchSize. */ public int getMaxBatchSize() { return this.maxBatchSize; } /** @param maxBatchSize the maxBatchSize to set. */ public void setMaxBatchSize(int maxBatchSize) { this.maxBatchSize = maxBatchSize; } }
/** @author Paul Ferraro */ public class SQLStateManagerFactory extends GenericObjectPoolConfiguration implements StateManagerFactory { private static final long serialVersionUID = -544548607415128414L; private Logger logger = LoggerFactory.getLogger(this.getClass()); enum EmbeddedVendor { H2("jdbc:h2:{1}/{0}"), HSQLDB("jdbc:hsqldb:{1}/{0}"), DERBY("jdbc:derby:{1}/{0};create=true"); final String pattern; EmbeddedVendor(String pattern) { this.pattern = pattern; } } private String urlPattern = this.defaultUrlPattern(); private String user; private String password; private String defaultUrlPattern() { for (EmbeddedVendor vendor : EmbeddedVendor.values()) { String url = MessageFormat.format(vendor.pattern, "test", Strings.HA_JDBC_HOME); try { for (Driver driver : Collections.list(DriverManager.getDrivers())) { if (driver.acceptsURL(url)) { return vendor.pattern; } } } catch (SQLException e) { // Skip vendor } } return null; } @Override public String getId() { return "sql"; } /** * {@inheritDoc} * * @see net.sf.hajdbc.state.StateManagerFactory#createStateManager(net.sf.hajdbc.DatabaseCluster) */ @Override public <Z, D extends Database<Z>> StateManager createStateManager(DatabaseCluster<Z, D> cluster) { if (this.urlPattern == null) { throw new IllegalArgumentException( "No embedded database driver was detected on the classpath."); } String url = MessageFormat.format(this.urlPattern, cluster.getId(), Strings.HA_JDBC_HOME); DriverDatabase database = new DriverDatabase(); database.setLocation(url); database.setUser(this.user); database.setPassword(this.password); this.logger.log( Level.INFO, "State for database cluster {0} will be persisted to {1}", cluster, url); return new SQLStateManager<Z, D>(cluster, database, new GenericObjectPoolFactory(this)); } public String getUrlPattern() { return this.urlPattern; } public void setUrlPattern(String urlPattern) { this.urlPattern = urlPattern; } public String getUser() { return this.user; } public void setUser(String user) { this.user = user; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
/** @author Paul Ferraro */ public class LifecycleRegistry<K, V extends Lifecycle, C, E extends Exception> implements Registry<K, V, C, E> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final LifecycleRegistry.Store<K, RegistryEntry> store; final Factory<K, V, C, E> factory; final ExceptionFactory<E> exceptionFactory; public LifecycleRegistry( Factory<K, V, C, E> factory, RegistryStoreFactory<K> storeFactory, ExceptionFactory<E> exceptionFactory) { this.store = storeFactory.createStore(); this.factory = factory; this.exceptionFactory = exceptionFactory; } /** * {@inheritDoc} * * @see net.sf.hajdbc.util.concurrent.Registry#get(java.lang.Object, java.lang.Object) */ @Override public V get(K key, C context) throws E { RegistryEntry entry = this.store.get(key); if (entry != null) { return entry.getValue(); } V value = this.factory.create(key, context); entry = new RegistryEntry(value); RegistryEntry existing = this.store.setIfAbsent(key, entry); if (existing != null) { return existing.getValue(); } try { value.start(); entry.started(); return value; } catch (Exception e) { this.store.clear(key); try { value.stop(); } catch (Exception re) { this.logger.log(Level.INFO, re); } throw this.exceptionFactory.createException(e); } } /** * {@inheritDoc} * * @see net.sf.hajdbc.util.concurrent.Registry#remove(java.lang.Object) */ @Override public void remove(K key) throws E { RegistryEntry entry = this.store.clear(key); if (entry != null) { entry.getValue().stop(); } } private class RegistryEntry { private final V value; private final AtomicReference<CountDownLatch> latchRef = new AtomicReference<CountDownLatch>(new CountDownLatch(1)); RegistryEntry(V value) { this.value = value; } V getValue() throws E { CountDownLatch latch = this.latchRef.get(); if (latch != null) { try { if (!latch.await( LifecycleRegistry.this.factory.getTimeout(), LifecycleRegistry.this.factory.getTimeoutUnit())) { throw LifecycleRegistry.this.exceptionFactory.createException(new TimeoutException()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw LifecycleRegistry.this.exceptionFactory.createException(e); } } return this.value; } void started() { CountDownLatch latch = this.latchRef.getAndSet(null); if (latch != null) { latch.countDown(); } } } }