/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.data.management.retention; import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.hadoop.fs.FileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Meter; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.io.Closer; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import gobblin.util.AzkabanTags; import gobblin.configuration.State; import gobblin.data.management.retention.dataset.CleanableDataset; import gobblin.data.management.retention.profile.MultiCleanableDatasetFinder; import gobblin.dataset.Dataset; import gobblin.dataset.DatasetsFinder; import gobblin.instrumented.Instrumentable; import gobblin.instrumented.Instrumented; import gobblin.metrics.GobblinMetrics; import gobblin.metrics.MetricContext; import gobblin.metrics.Tag; import gobblin.metrics.event.EventSubmitter; import gobblin.util.ExecutorsUtils; import gobblin.util.RateControlledFileSystem; import gobblin.util.executors.ScalingThreadPoolExecutor; /** * Finds existing versions of datasets and cleans old or deprecated versions. */ public class DatasetCleaner implements Instrumentable, Closeable { public static final String CONFIGURATION_KEY_PREFIX = "gobblin.retention."; public static final String MAX_CONCURRENT_DATASETS_CLEANED = CONFIGURATION_KEY_PREFIX + "max.concurrent.datasets.cleaned"; public static final String DATASET_CLEAN_HDFS_CALLS_PER_SECOND_LIMIT = CONFIGURATION_KEY_PREFIX + "hdfs.calls.per.second.limit"; public static final String DEFAULT_MAX_CONCURRENT_DATASETS_CLEANED = "100"; private static Logger LOG = LoggerFactory.getLogger(DatasetCleaner.class); private final DatasetsFinder<Dataset> datasetFinder; private final ListeningExecutorService service; private final Closer closer; private final boolean isMetricEnabled; private MetricContext metricContext; private final EventSubmitter eventSubmitter; private Optional<Meter> datasetsCleanSuccessMeter = Optional.absent(); private Optional<Meter> datasetsCleanFailureMeter = Optional.absent(); private Optional<CountDownLatch> finishCleanSignal; private final List<Throwable> throwables; public DatasetCleaner(FileSystem fs, Properties props) throws IOException { this.closer = Closer.create(); try { FileSystem optionalRateControlledFs = fs; if (props.contains(DATASET_CLEAN_HDFS_CALLS_PER_SECOND_LIMIT)) { optionalRateControlledFs = this.closer.register(new RateControlledFileSystem(fs, Long.parseLong(props.getProperty(DATASET_CLEAN_HDFS_CALLS_PER_SECOND_LIMIT)))); ((RateControlledFileSystem) optionalRateControlledFs).startRateControl(); } this.datasetFinder = new MultiCleanableDatasetFinder(optionalRateControlledFs, props); } catch (NumberFormatException exception) { throw new IOException(exception); } catch (ExecutionException exception) { throw new IOException(exception); } ExecutorService executor = ScalingThreadPoolExecutor.newScalingThreadPool(0, Integer.parseInt(props.getProperty(MAX_CONCURRENT_DATASETS_CLEANED, DEFAULT_MAX_CONCURRENT_DATASETS_CLEANED)), 100, ExecutorsUtils.newThreadFactory(Optional.of(LOG), Optional.of("Dataset-cleaner-pool-%d"))); this.service = ExecutorsUtils.loggingDecorator(executor); List<Tag<?>> tags = Lists.newArrayList(); tags.addAll(Tag.fromMap(AzkabanTags.getAzkabanTags())); // TODO -- Remove the dependency on gobblin-core after new Gobblin Metrics does not depend on gobblin-core. this.metricContext = this.closer.register(Instrumented.getMetricContext(new State(props), DatasetCleaner.class, tags)); this.isMetricEnabled = GobblinMetrics.isEnabled(props); this.eventSubmitter = new EventSubmitter.Builder(this.metricContext, RetentionEvents.NAMESPACE).build(); this.throwables = Lists.newArrayList(); } /** * Perform the cleanup of old / deprecated dataset versions. * @throws IOException */ public void clean() throws IOException { List<Dataset> dataSets = this.datasetFinder.findDatasets(); this.finishCleanSignal = Optional.of(new CountDownLatch(dataSets.size())); for (final Dataset dataset : dataSets) { ListenableFuture<Void> future = this.service.submit(new Callable<Void>() { @Override public Void call() throws Exception { if (dataset instanceof CleanableDataset) { ((CleanableDataset) dataset).clean(); } return null; } }); Futures.addCallback(future, new FutureCallback<Void>() { @Override public void onFailure(Throwable throwable) { DatasetCleaner.this.finishCleanSignal.get().countDown(); LOG.warn("Exception caught when cleaning " + dataset.datasetURN() + ".", throwable); DatasetCleaner.this.throwables.add(throwable); Instrumented.markMeter(DatasetCleaner.this.datasetsCleanFailureMeter); DatasetCleaner.this.eventSubmitter.submit(RetentionEvents.CleanFailed.EVENT_NAME, ImmutableMap.of(RetentionEvents.CleanFailed.FAILURE_CONTEXT_METADATA_KEY, ExceptionUtils.getFullStackTrace(throwable), RetentionEvents.DATASET_URN_METADATA_KEY, dataset.datasetURN())); } @Override public void onSuccess(Void arg0) { DatasetCleaner.this.finishCleanSignal.get().countDown(); LOG.info("Successfully cleaned: " + dataset.datasetURN()); Instrumented.markMeter(DatasetCleaner.this.datasetsCleanSuccessMeter); } }); } } @Override public void close() throws IOException { try { if (this.finishCleanSignal.isPresent()) { this.finishCleanSignal.get().await(); } if (!this.throwables.isEmpty()) { for (Throwable t : this.throwables) { LOG.error("Failed clean due to ", t); } throw new RuntimeException("Clean failed for one or more datasets"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Not all datasets finish cleanning", e); } finally { ExecutorsUtils.shutdownExecutorService(this.service, Optional.of(LOG)); this.closer.close(); } } @Override public MetricContext getMetricContext() { return this.metricContext; } @Override public boolean isInstrumentationEnabled() { return this.isMetricEnabled; } /** Default with no additional tags */ @Override public List<Tag<?>> generateTags(State state) { return Lists.newArrayList(); } @Override public void switchMetricContext(List<Tag<?>> tags) { this.metricContext = this.closer .register(Instrumented.newContextFromReferenceContext(this.metricContext, tags, Optional.<String> absent())); this.regenerateMetrics(); } @Override public void switchMetricContext(MetricContext context) { this.metricContext = context; this.regenerateMetrics(); } /** * Generates metrics for the instrumentation of this class. */ protected void regenerateMetrics() { if (isInstrumentationEnabled()) { this.datasetsCleanFailureMeter = Optional.of(this.metricContext.meter(DatasetCleanerMetrics.DATASETS_CLEAN_FAILURE)); this.datasetsCleanSuccessMeter = Optional.of(this.metricContext.meter(DatasetCleanerMetrics.DATASETS_CLEAN_SUCCESS)); } } public static class DatasetCleanerMetrics { public static final String DATASETS_CLEAN_SUCCESS = "gobblin.retention.datasets.clean.success"; public static final String DATASETS_CLEAN_FAILURE = "gobblin.retention.datasets.clean.failure"; } }