/* * Copyright 2008-2013 LinkedIn, Inc * * 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. */ package voldemort.server.scheduler; import java.util.concurrent.atomic.AtomicLong; import org.apache.log4j.Logger; import voldemort.VoldemortException; import voldemort.annotations.jmx.JmxGetter; import voldemort.server.storage.ScanPermitWrapper; import voldemort.store.StorageEngine; import voldemort.store.StoreDefinition; import voldemort.store.metadata.MetadataStore; import voldemort.store.metadata.MetadataStore.VoldemortState; import voldemort.utils.ClosableIterator; import voldemort.utils.EventThrottler; import voldemort.utils.Pair; import voldemort.utils.Time; import voldemort.utils.Utils; import voldemort.versioning.VectorClock; import voldemort.versioning.Versioned; /** * Expire old data * * */ public class DataCleanupJob<K, V, T> implements Runnable { private static final Logger logger = Logger.getLogger(DataCleanupJob.class); private final StorageEngine<K, V, T> store; private final ScanPermitWrapper cleanupPermits; private final String storeName; private final Time time; private long totalEntriesScanned = 0; private AtomicLong scanProgressThisRun; private long totalEntriesDeleted = 0; private AtomicLong deleteProgressThisRun; private MetadataStore metadataStore; public DataCleanupJob(StorageEngine<K, V, T> store, ScanPermitWrapper cleanupPermits, String storeName, Time time, MetadataStore metadataStore) { this.store = Utils.notNull(store); this.cleanupPermits = Utils.notNull(cleanupPermits); this.storeName = Utils.notNull(storeName); this.time = Utils.notNull(time); this.metadataStore = Utils.notNull(metadataStore); this.scanProgressThisRun = new AtomicLong(0); this.deleteProgressThisRun = new AtomicLong(0); } private boolean isServerInNormalState() { if(metadataStore != null && metadataStore.getServerStateUnlocked() != VoldemortState.NORMAL_SERVER) { logger.info("Datacleanup on store " + store.getName() + " skipped since server is not normal.."); return false; } return true; } private boolean isServerInOfflineState() { if(metadataStore != null && metadataStore.getServerStateUnlocked() != VoldemortState.OFFLINE_SERVER) { logger.info("Datacleanup on store " + store.getName() + " skipped since server is not offline.."); return false; } return true; } @Override public void run() { // if the server is neither normal nor offline , skip this run. if(!isServerInNormalState() && !isServerInOfflineState()) { return; } StoreDefinition storeDef = null; try { storeDef = MetadataStore.getStoreDef(storeName, metadataStore); } catch (VoldemortException ex) { logger.info("Error retrieving store " + storeName + " for data cleanup job ", ex); return; } Integer retentionDays = storeDef.getRetentionDays(); if (retentionDays == null || retentionDays <= 0) { logger.info("Store " + storeName + " does not have retention period set, skipping cleanup job . RetentionDays " + retentionDays); return; } long maxAgeMs = retentionDays * Time.MS_PER_DAY; logger.info("Store " + storeName + " cleanup job is starting with RetentionDays " + retentionDays); acquireCleanupPermit(scanProgressThisRun, deleteProgressThisRun); ClosableIterator<Pair<K, Versioned<V>>> iterator = null; try { int maxReadRate = storeDef.hasRetentionScanThrottleRate() ? storeDef.getRetentionScanThrottleRate() : Integer.MAX_VALUE; EventThrottler throttler = new EventThrottler(maxReadRate); store.beginBatchModifications(); logger.info("Starting data cleanup on store \"" + store.getName() + "\"..."); long now = time.getMilliseconds(); iterator = store.entries(); while(iterator.hasNext()) { // check if we have been interrupted if(Thread.currentThread().isInterrupted()) { logger.info("Datacleanup job halted."); return; } final long INETERVAL = 10000; long entriesScanned = scanProgressThisRun.get(); if(entriesScanned % INETERVAL == 0) { if(!isServerInNormalState() && !isServerInOfflineState()) { return; } } scanProgressThisRun.incrementAndGet(); Pair<K, Versioned<V>> keyAndVal = iterator.next(); VectorClock clock = (VectorClock) keyAndVal.getSecond().getVersion(); if(now - clock.getTimestamp() > maxAgeMs) { store.delete(keyAndVal.getFirst(), clock); final long entriesDeleted = this.deleteProgressThisRun.incrementAndGet(); if(logger.isDebugEnabled() && entriesDeleted % INETERVAL == 0) { logger.debug("Deleted item " + this.deleteProgressThisRun.get()); } } // throttle on number of entries. throttler.maybeThrottle(1); } // log the total items scanned, so we will get an idea of data // growth in a cheap, periodic way logger.info("Data cleanup on store \"" + store.getName() + "\" is complete; " + this.deleteProgressThisRun.get() + " items deleted. " + scanProgressThisRun.get() + " items scanned"); } catch(Exception e) { logger.error("Error in data cleanup job for store " + store.getName() + ": ", e); } finally { closeIterator(iterator); logger.info("Releasing lock after data cleanup on \"" + store.getName() + "\"."); this.cleanupPermits.release(this.getClass().getCanonicalName()); synchronized(this) { totalEntriesScanned += scanProgressThisRun.get(); scanProgressThisRun.set(0); totalEntriesDeleted += deleteProgressThisRun.get(); deleteProgressThisRun.set(0); } store.endBatchModifications(); } } private void closeIterator(ClosableIterator<Pair<K, Versioned<V>>> iterator) { try { if(iterator != null) iterator.close(); } catch(Exception e) { logger.error("Error in closing iterator " + store.getName() + " ", e); } } private void acquireCleanupPermit(AtomicLong scanProgress, AtomicLong deleteProgress) { logger.info("Acquiring lock to perform data cleanup on \"" + store.getName() + "\"."); try { this.cleanupPermits.acquire(scanProgress, deleteProgress, this.getClass() .getCanonicalName()); } catch(InterruptedException e) { throw new IllegalStateException("Datacleanup interrupted while waiting for cleanup permit.", e); } } @JmxGetter(name = "numEntriesScanned", description = "Returns number of entries scanned") public synchronized long getEntriesScanned() { return totalEntriesScanned + scanProgressThisRun.get(); } @JmxGetter(name = "numEntriesDeleted", description = "Returns number of entries deleted") public synchronized long getEntriesDeleted() { return totalEntriesDeleted + deleteProgressThisRun.get(); } }