/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.server.purge; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; import javax.transaction.Status; import javax.transaction.UserTransaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.db.DatabaseType; import org.rhq.core.db.DatabaseTypeFactory; import org.rhq.core.util.jdbc.JDBCUtil; import org.rhq.core.util.stream.StreamUtil; /** * A template for purging data tables.<br> * <br> * When the {@link #execute()} method is called, row keys are selected and stored in a file. Then the corresponding rows * are deleted in batches. * * @author Thomas Segismont */ abstract class PurgeTemplate<KEY extends Serializable> { private static final Log LOG = LogFactory.getLog(PurgeTemplate.class); private static final String BATCH_SIZE_SYSTEM_PROPERTY = "org.rhq.enterprise.server.purge.PurgeTemplate.BATCH_SIZE"; private static final int BATCH_SIZE = Integer.getInteger(BATCH_SIZE_SYSTEM_PROPERTY, 30000); static { LOG.info(BATCH_SIZE_SYSTEM_PROPERTY + " = " + BATCH_SIZE); } protected final DataSource dataSource; protected final UserTransaction userTransaction; protected final DatabaseType databaseType; /** * @param dataSource the source of JDBC connections to the database * @param userTransaction the transaction management interface */ public PurgeTemplate(DataSource dataSource, UserTransaction userTransaction) { this.dataSource = dataSource; this.userTransaction = userTransaction; databaseType = DatabaseTypeFactory.getDefaultDatabaseType(); } /** * @return the name of the data being purged, used for logging purpose */ protected abstract String getEntityName(); public int execute() { int deleted = 0; KeysInfo keysInfo = null; ObjectInputStream keysStream = null; try { keysInfo = loadKeys(); if (LOG.isDebugEnabled()) { LOG.debug("Loaded " + keysInfo.count + " key(s) of " + getEntityName()); } keysStream = new ObjectInputStream(new BufferedInputStream(new FileInputStream(keysInfo.keysFile))); List<KEY> selectedKeys = new ArrayList<KEY>(BATCH_SIZE); for (int i = 1; i <= keysInfo.count; i++) { @SuppressWarnings("unchecked") KEY key = (KEY) keysStream.readObject(); selectedKeys.add(key); if (selectedKeys.size() == BATCH_SIZE || i == keysInfo.count) { if (LOG.isDebugEnabled()) { LOG.debug("Deleting " + selectedKeys.size() + " row(s) of " + getEntityName()); } deleted += deleteRows(selectedKeys); selectedKeys.clear(); } } } catch (Exception e) { LOG.error(getEntityName() + ": could not fully process the batched purge", e); } finally { rollbackIfTransactionActive(); StreamUtil.safeClose(keysStream); if (keysInfo != null && keysInfo.keysFile != null) { keysInfo.keysFile.delete(); } } return deleted; } private KeysInfo loadKeys() throws Exception { File keysFile = File.createTempFile(getClass().getSimpleName(), null); int count = 0; ObjectOutputStream objectOutputStream = null; Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(keysFile))); userTransaction.begin(); String findRowKeysQuery = getFindRowKeysQuery(databaseType); connection = dataSource.getConnection(); preparedStatement = connection.prepareStatement(findRowKeysQuery); setFindRowKeysQueryParams(preparedStatement); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { if(count % BATCH_SIZE == 0) { objectOutputStream.reset(); } objectOutputStream.writeObject(getKeyFromResultSet(resultSet)); count++; } userTransaction.commit(); } finally { JDBCUtil.safeClose(connection, preparedStatement, resultSet); StreamUtil.safeClose(objectOutputStream); rollbackIfTransactionActive(); } return new KeysInfo(keysFile, count); } /** * @return the query selecting row keys */ protected abstract String getFindRowKeysQuery(DatabaseType databaseType); /** * Set the row keys selection query parameters. * * @param preparedStatement the prepared statement created for the row keys selection query * * @throws SQLException */ protected abstract void setFindRowKeysQueryParams(PreparedStatement preparedStatement) throws SQLException; /** * @return the row key extracted from the <code>resultSet</code> columns values * * @throws SQLException */ protected abstract KEY getKeyFromResultSet(ResultSet resultSet) throws SQLException; protected int deleteRows(List<KEY> selectedKeys) throws Exception { Connection connection = null; PreparedStatement preparedStatement = null; try { userTransaction.begin(); String deleteRowByKeyQuery = getDeleteRowByKeyQuery(databaseType); connection = dataSource.getConnection(); preparedStatement = connection.prepareStatement(deleteRowByKeyQuery); for (KEY key : selectedKeys) { setDeleteRowByKeyQueryParams(preparedStatement, key); preparedStatement.addBatch(); } int[] batchResults = preparedStatement.executeBatch(); userTransaction.commit(); return evalDeletedRows(batchResults); } finally { JDBCUtil.safeClose(connection, preparedStatement, null); rollbackIfTransactionActive(); } } /** * @return the query deleting a row by key */ protected abstract String getDeleteRowByKeyQuery(DatabaseType databaseType); /** * Set the deletion query parameters. Implementations should use the <code>key</code> provided. * * @param preparedStatement the prepared statement created for the row deletion query * @param key the row key object representation * * @throws SQLException */ protected abstract void setDeleteRowByKeyQueryParams(PreparedStatement preparedStatement, KEY key) throws SQLException; protected void rollbackIfTransactionActive() { try { if (userTransaction.getStatus() == Status.STATUS_ACTIVE) { userTransaction.rollback(); } } catch (Throwable ignore) { } } protected int evalDeletedRows(int[] results) { int total = 0, failed = 0; for (int result : results) { if (result == Statement.EXECUTE_FAILED) { failed++; } else if (result == Statement.SUCCESS_NO_INFO) { // Pre v12 Oracle servers return -2 because they don't track batch update counts total++; } else { total += result; } } if (failed > 0) { LOG.warn(getEntityName() + ": " + failed + " row(s) not purged"); } return total; } private static class KeysInfo { final File keysFile; final int count; private KeysInfo(File keysFile, int count) { this.keysFile = keysFile; this.count = count; } } }