package com.linkedin.databus.bootstrap.common; import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.apache.log4j.Logger; import com.linkedin.databus.bootstrap.common.BootstrapCleanerStaticConfig.BootstrapDBType; import com.linkedin.databus.bootstrap.common.BootstrapCleanerStaticConfig.RetentionStaticConfig; import com.linkedin.databus.bootstrap.common.BootstrapDBMetaDataDAO.SourceStatusInfo; import com.linkedin.databus.core.DatabusThreadBase; import com.linkedin.databus.core.DbusConstants; import com.linkedin.databus.core.DbusEventFactory; import com.linkedin.databus.core.DbusEventV1Factory; import com.linkedin.databus2.core.container.request.BootstrapDatabaseTooOldException; /* * * Copyright 2013 LinkedIn Corp. All rights reserved * * 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. * */ public class BootstrapDBSingleSourceCleaner implements Runnable { public static final String MODULE = BootstrapDBSingleSourceCleaner.class.getName(); public final Logger LOG; private final String _name; private final String _source; private final DatabusThreadBase _applier; private final BootstrapCleanerStaticConfig _bootstrapCleanerStaticConfig; private final BootstrapReadOnlyConfig _bootstrapReadOnlyConfig; private BootstrapDBMetaDataDAO _bootstrapDao = null; private SourceStatusInfo _sourceStatusInfo = null; private final BootstrapDBCleanerQueryHelper _bootstrapDBCleanerQueryHelper; private final BootstrapDBCleanerQueryExecutor _bootstrapDBCleanerQueryExecutor; private final DbusEventFactory _eventFactory; private BootstrapLogInfo _lastValidLog; private volatile boolean _isCleaning = false; private static final AtomicInteger _numCleanersRunning = new AtomicInteger(0); private static final AtomicInteger _numCleanersRunningHWM = new AtomicInteger(0); public BootstrapDBSingleSourceCleaner(String name, String source, DatabusThreadBase applier, BootstrapCleanerStaticConfig bootstrapCleanerStaticConfig, BootstrapReadOnlyConfig bootstrapReadOnlyConfig) throws SQLException { _name = name; _source = source; _applier = applier; _bootstrapCleanerStaticConfig = bootstrapCleanerStaticConfig; _bootstrapReadOnlyConfig = bootstrapReadOnlyConfig; LOG = Logger.getLogger(name); Connection conn = getOrCreateConnection(); if (null != source) { try { List<SourceStatusInfo> ssil = _bootstrapDao.getSourceIdAndStatusFromName(Arrays.asList(source), false); assert(ssil.size() == 1); _sourceStatusInfo = ssil.get(0); } catch (BootstrapDatabaseTooOldException bto) { LOG.error( "Not expected to receive this exception as activeCheck is turned-off", bto); throw new RuntimeException(bto); } } _bootstrapDBCleanerQueryHelper = BootstrapDBCleanerQueryHelper.getInstance(); _bootstrapDBCleanerQueryExecutor = new BootstrapDBCleanerQueryExecutor(_name, conn, _bootstrapDBCleanerQueryHelper); _eventFactory = new DbusEventV1Factory(); } /* * @return a bootstrapDB connection object. Note: The connection object is * still owned by BootstrapConn. SO dont close it */ private Connection getOrCreateConnection() throws SQLException { Connection conn = null; if (_bootstrapDao == null) { LOG.info("<<<< Creating Bootstrap Connection!! >>>>"); BootstrapConn dbConn = new BootstrapConn(); final boolean autoCommit = true; try { _bootstrapDao = new BootstrapDBMetaDataDAO(dbConn, _bootstrapReadOnlyConfig.getBootstrapDBHostname(), _bootstrapReadOnlyConfig.getBootstrapDBUsername(), _bootstrapReadOnlyConfig.getBootstrapDBPassword(), _bootstrapReadOnlyConfig.getBootstrapDBName(), autoCommit); dbConn.initBootstrapConn(autoCommit, _bootstrapReadOnlyConfig.getBootstrapDBUsername(), _bootstrapReadOnlyConfig.getBootstrapDBPassword(), _bootstrapReadOnlyConfig.getBootstrapDBHostname(), _bootstrapReadOnlyConfig.getBootstrapDBName()); } catch (Exception e) { LOG.fatal("Unable to open BootstrapDB Connection !!", e); throw new RuntimeException( "Got exception when getting bootstrap DB Connection.", e); } } try { conn = _bootstrapDao.getBootstrapConn().getDBConn(); } catch (SQLException e) { LOG.fatal("Not able to open BootstrapDB Connection !!", e); throw new RuntimeException( "Got exception when getting bootstrap DB Connection.", e); } return conn; } @Override public void run() { doClean(); } private void doClean() { try { incCleanerStats(); SourceStatusInfo s = _sourceStatusInfo; { assert(s.getSrcName().equals(_source)); BootstrapDBType type = _bootstrapCleanerStaticConfig.getBootstrapType(s.getSrcName()); LOG.info("Cleaner running for source :" + s.getSrcName() + "(" + s.getSrcId() + ") with bootstrapDB type :" + type); BootstrapLogInfo logInfo = _bootstrapDBCleanerQueryExecutor.getThresholdWindowSCN(type, s.getSrcId()); if (null == logInfo) { LOG.info("No WindowSCN. Nothing to cleanup for source : " + s.getSrcName()); return; } LOG.info("LOG info with lowest windowSCN :" + logInfo); LOG.info("Begin phase 1 : Gather candidate loginfo :"); List<BootstrapLogInfo> candidateLogsInfo = _bootstrapDBCleanerQueryExecutor.getCandidateLogsInfo( logInfo.getMinWindowSCN(), (short) (s.getSrcId())); if ((null == candidateLogsInfo) || (candidateLogsInfo.isEmpty())) { LOG.info("No logs to cleanup for source :" + s.getSrcName() + "(" + s.getSrcId() + ")"); return; } LOG.info("End phase 1 : Gather candidate loginfo :"); LOG.info("Initial Candidate Set for Source :" + s.getSrcName() + " is :" + candidateLogsInfo); RetentionStaticConfig rConf = _bootstrapCleanerStaticConfig.getRetentionConfig(s .getSrcName()); LOG.info("Retention Config for source :" + s.getSrcName() + " is :" + rConf); LOG.info("Begin phase 2 : Filter based on retention config :"); long scn = filterCandidateLogInfo((short) s.getSrcId(), candidateLogsInfo, _bootstrapCleanerStaticConfig.getRetentionConfig(s.getSrcName())); LOG.info("Log tables to be deleted for source :" + s.getSrcName() + "(" + s.getSrcId() + ") are :" + candidateLogsInfo + ", Max SCN of deleted logs:" + scn); LOG.info("End phase 2 : Filter based on retention config :"); if ((scn <= 0) || (candidateLogsInfo.isEmpty())) { LOG.info("Source :" + s.getSrcName() + "(" + s.getSrcId() + ") No log tables to be deleted !! MaxSCN : " + scn + ", candidateLogs :" + candidateLogsInfo); return; } LOG.info("Begin phase 3 : Updating Meta Info :"); BootstrapLogInfo firstValidLog = _bootstrapDBCleanerQueryExecutor.getFirstLogTableWithGreaterSCN( (short) s.getSrcId(), scn); _bootstrapDBCleanerQueryExecutor.updateSource(firstValidLog); LOG.info("End phase 3 : Updating Meta Info :"); LOG.info("Begin phase 4 : Deleting Log tables :"); // marking logs as done; if any failures; there is a chance that the // logs have to be cleaned up later _bootstrapDBCleanerQueryExecutor.markDeleted(candidateLogsInfo); _bootstrapDBCleanerQueryExecutor.dropTables(candidateLogsInfo); LOG.info("End phase 4 : Deleting Log tables :"); if ((_bootstrapCleanerStaticConfig.getBootstrapType(s.getSrcName()) == BootstrapDBType.BOOTSTRAP_CATCHUP_APPLIER_RUNNING) && ((_applier != null) || _bootstrapCleanerStaticConfig.forceTabTableCleanup(s .getSrcName()))) { LOG.info("Source :" + s.getSrcName() + "(" + s.getSrcId() + ") is running in catchup_applier_running mode. " + "Will delete all rows whose scn is less than or equal to " + scn); if ((null != _applier) && (_applier.isAlive())) { LOG.info("Begin phase 5 : Pausing Applier and deleting Rows from tab table :"); LOG.info("Requesting applier to pause !!"); _applier.pause(); LOG.info("Applier paused !!"); } try { // mark ahead of time; if this doesn't work this time; it will next // cycle _bootstrapDao.updateMinScnOfSnapshot(s.getSrcId(), scn); String srcTable = _bootstrapDBCleanerQueryHelper.getSrcTable(s.getSrcId()); int numRowsDeleted = _bootstrapDBCleanerQueryExecutor.deleteTable(srcTable, scn); LOG.info("Number of Rows deleted for source :" + s.getSrcName() + "(" + s.getSrcId() + ") :" + numRowsDeleted); if (numRowsDeleted > 0 && _bootstrapCleanerStaticConfig.isOptimizeTableEnabled(s.getSrcName())) { LOG.info("Optimizing table to reclaim space for source :" + s.getSrcName() + "(" + s.getSrcId() + ")"); _bootstrapDBCleanerQueryExecutor.optimizeTable(srcTable); } } finally { if ((null != _applier) && (_applier.isAlive())) { LOG.info("Requesting applier to resume !!"); _applier.unpause(); LOG.info("Applier resumed !!"); } } LOG.info("End phase 5 : Deleting Rows from tab table :"); } LOG.info("Cleaner done for source :" + s.getSrcName() + "(" + s.getSrcId() + ")"); } } catch (SQLException ex) { LOG.error("Got SQL exception while cleaning bootstrapDB !!", ex); } catch (InterruptedException ie) { LOG.error("Got interrupted exception while cleaning bootstrapDB !!", ie); } finally { decCleanerStats(); } } public BootstrapDBMetaDataDAO getBootstrapDao() { return _bootstrapDao; } public boolean isCleanerRunning() { return _isCleaning; } public String getName() { return _name; } public void close() { if (_bootstrapDao != null) { _bootstrapDao.close(); _bootstrapDao = null; } } /** * A diagnotic to expose the number of cleaners running at a given moment */ public static int getNumCleanersRunningHWM() { return _numCleanersRunningHWM.get(); } /** * Return the milli-second threshold for delete criteria. * * @param config * RetentionConfig * @return milliSecThreshold */ private long getMilliSecTime(RetentionStaticConfig config) { long qty = config.getRetentionQuantity(); long milliSecQty = -1; switch (config.getRetentiontype()) { case RETENTION_SECONDS: milliSecQty = qty * DbusConstants.NUM_MSECS_IN_SEC; break; default: throw new RuntimeException("Retention Config (" + config + ") expected to be time based but is not !!"); } return milliSecQty; } private long filterCandidateLogInfo(short srcId, List<BootstrapLogInfo> candidateLogsInfo, RetentionStaticConfig config) throws SQLException { switch (config.getRetentiontype()) { case NO_CLEANUP: return -1; case RETENTION_LOGS: { Iterator<BootstrapLogInfo> itr = candidateLogsInfo.iterator(); BootstrapLogInfo lastValidLog = null; int i = 0; while (i < config.getRetentionQuantity() && itr.hasNext()) { BootstrapLogInfo log = itr.next(); LOG.info("Removing the log table :" + log.getLogTable() + " from the delete List as it is too recent. Retaining :" + config.getRetentionQuantity() + " logs"); itr.remove(); lastValidLog = log; i++; } _lastValidLog = lastValidLog; break; } case RETENTION_SECONDS: { long quantity = config.getRetentionQuantity(); LOG.info("Retaining tables which could contain events which is less than " + quantity + " seconds old !!"); long currTs = System.currentTimeMillis() * DbusConstants.NUM_NSECS_IN_MSEC; long nanoSecQty = getMilliSecTime(config) * DbusConstants.NUM_NSECS_IN_MSEC; long threshold = (currTs - nanoSecQty); LOG.info("Removing tables from the delete-list whose last row has timestamp newer than :" + threshold + " nanosecs"); Iterator<BootstrapLogInfo> itr = candidateLogsInfo.iterator(); BootstrapLogInfo lastValidLog = null; LOG.info("Timestamp Threshold for src id :" + srcId + " is :" + threshold + ", Retention Config " + config + "(" + nanoSecQty + " nanosecs)"); while (itr.hasNext()) { BootstrapLogInfo log = itr.next(); long timestamp = _bootstrapDBCleanerQueryExecutor.getNanoTimestampOfLastEventinLog(log, _eventFactory); if (timestamp < threshold) { LOG.info("Reached the log table whose timestamp (" + timestamp + ") is less than the threshold (" + threshold + ")."); break; } else { LOG.info("Removing the log table :" + log.getLogTable() + " from the delete List as it is too recent. Last Event Timestamp :" + timestamp + ", threshold :" + threshold); lastValidLog = log; itr.remove(); } } _lastValidLog = lastValidLog; } break; } long scn = -1; if (!candidateLogsInfo.isEmpty()) scn = _bootstrapDBCleanerQueryExecutor.getSCNOfLastEventinLog(candidateLogsInfo.get(0), _eventFactory); return scn; } private void incCleanerStats() { _isCleaning = true; // Update HWM int curCleanersHwm = _numCleanersRunningHWM.get(); // Increment internal metrics used for measuring parallelism int curCleaners = _numCleanersRunning.incrementAndGet(); if (curCleanersHwm < curCleaners) { _numCleanersRunningHWM.set(curCleaners); } } private void decCleanerStats() { _isCleaning = false; // Decrement internal metrics used for measuring parallelism _numCleanersRunning.decrementAndGet(); } }