/*
* Copyright © 2014-2015 Cask Data, 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 co.cask.cdap.logging.write;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.guice.ConfigModule;
import co.cask.cdap.common.guice.LocationRuntimeModule;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.logging.LoggingContext;
import co.cask.cdap.data.runtime.DataSetsModules;
import co.cask.cdap.data.runtime.SystemDatasetRuntimeModule;
import co.cask.cdap.data.runtime.TransactionExecutorModule;
import co.cask.cdap.logging.LoggingConfiguration;
import co.cask.cdap.logging.context.FlowletLoggingContext;
import co.cask.tephra.TransactionManager;
import co.cask.tephra.runtime.TransactionModules;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.util.List;
import java.util.Set;
/**
* Test LogCleanup class.
*/
public class LogCleanupTest {
private static final Logger LOG = LoggerFactory.getLogger(LogCleanupTest.class);
@ClassRule
public static final TemporaryFolder TEMP_FOLDER = new TemporaryFolder();
private static final int RETENTION_DURATION_MS = 100000;
private static Injector injector;
private static TransactionManager txManager;
private static String logBaseDir;
private static String namespacesDir;
@BeforeClass
public static void init() throws Exception {
Configuration hConf = HBaseConfiguration.create();
final CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMP_FOLDER.newFolder().getAbsolutePath());
logBaseDir = cConf.get(LoggingConfiguration.LOG_BASE_DIR);
namespacesDir = cConf.get(Constants.Namespace.NAMESPACES_DIR);
injector = Guice.createInjector(
new ConfigModule(cConf, hConf),
new LocationRuntimeModule().getInMemoryModules(),
new TransactionModules().getInMemoryModules(),
new TransactionExecutorModule(),
new DataSetsModules().getInMemoryModules(),
new SystemDatasetRuntimeModule().getInMemoryModules()
);
txManager = injector.getInstance(TransactionManager.class);
txManager.startAndWait();
}
@AfterClass
public static void finish() {
txManager.stopAndWait();
}
@Test
public void testCleanup() throws Exception {
FileMetaDataManager fileMetaDataManager = injector.getInstance(FileMetaDataManager.class);
// Create base dir
LocationFactory locationFactory = injector.getInstance(LocationFactory.class);
Location baseDir = locationFactory.create(TEMP_FOLDER.newFolder().toURI());
// Deletion boundary
long deletionBoundary = System.currentTimeMillis() - RETENTION_DURATION_MS;
LOG.info("deletionBoundary = {}", deletionBoundary);
// Setup directories
LoggingContext dummyContext = new FlowletLoggingContext("ns", "app", "flw", "flwt", "run", "instance");
Location namespacedLogsDir = baseDir.append(namespacesDir).append("ns").append(logBaseDir);
Location contextDir = namespacedLogsDir.append("app").append("flw");
List<Location> toDelete = Lists.newArrayList();
for (int i = 0; i < 5; ++i) {
toDelete.add(contextDir.append("2012-12-1" + i + "/del-1"));
toDelete.add(contextDir.append("2012-12-1" + i + "/del-2"));
toDelete.add(contextDir.append("2012-12-1" + i + "/del-3"));
toDelete.add(contextDir.append("2012-12-1" + i + "/del-4"));
toDelete.add(contextDir.append("del-1"));
}
Assert.assertFalse(toDelete.isEmpty());
List<Location> notDelete = Lists.newArrayList();
for (int i = 0; i < 5; ++i) {
toDelete.add(contextDir.append("2012-12-2" + i + "/del-5"));
notDelete.add(contextDir.append("2012-12-2" + i + "/nodel-1"));
notDelete.add(contextDir.append("2012-12-2" + i + "/nodel-2"));
notDelete.add(contextDir.append("2012-12-2" + i + "/nodel-3"));
}
Assert.assertFalse(notDelete.isEmpty());
int counter = 0;
for (Location location : toDelete) {
fileMetaDataManager.writeMetaData(dummyContext, deletionBoundary - counter - 10000,
createFile(location));
counter++;
}
for (Location location : notDelete) {
fileMetaDataManager.writeMetaData(dummyContext, deletionBoundary + counter + 10000,
createFile(location));
counter++;
}
Assert.assertEquals(locationListsToString(toDelete, notDelete),
toDelete.size() + notDelete.size(), fileMetaDataManager.listFiles(dummyContext).size());
LogCleanup logCleanup = new LogCleanup(fileMetaDataManager, baseDir, namespacesDir, RETENTION_DURATION_MS);
logCleanup.run();
logCleanup.run();
for (Location location : toDelete) {
Assert.assertFalse("Location " + location + " is not deleted!", location.exists());
}
for (Location location : notDelete) {
Assert.assertTrue("Location " + location + " is deleted!", location.exists());
}
for (int i = 0; i < 5; ++i) {
Location delDir = contextDir.append("2012-12-1" + i);
Assert.assertFalse("Location " + delDir + " is not deleted!", delDir.exists());
}
}
@Test
public void testDeleteEmptyDir1() throws Exception {
// Create base dir
Location baseDir = injector.getInstance(LocationFactory.class).create(TEMP_FOLDER.newFolder().toURI());
// Create namespaced logs dirs
Location namespacedLogsDir1 = baseDir.append(namespacesDir).append("ns1").append(logBaseDir);
Location namespacedLogsDir2 = baseDir.append(namespacesDir).append("ns2").append(logBaseDir);
// Create dirs with files
Set<Location> files = Sets.newHashSet();
Set<Location> nonEmptyDirs = Sets.newHashSet();
for (int i = 0; i < 1; ++i) {
String name = String.valueOf(i);
files.add(createFile(namespacedLogsDir1.append(name)));
Location dir1 = createDir(namespacedLogsDir1.append("abc"));
files.add(dir1);
nonEmptyDirs.add(dir1);
files.add(createFile(namespacedLogsDir1.append("abc").append(name)));
files.add(createFile(namespacedLogsDir1.append("abc").append("def").append(name)));
Location dir2 = createDir(namespacedLogsDir2.append("def"));
files.add(dir2);
nonEmptyDirs.add(dir2);
files.add(createFile(namespacedLogsDir2.append("def").append(name)));
files.add(createFile(namespacedLogsDir2.append("def").append("hij").append(name)));
}
// Create empty dirs
Set<Location> emptyDirs = Sets.newHashSet();
for (int i = 0; i < 1; ++i) {
emptyDirs.add(createDir(namespacedLogsDir1.append("dir_" + i)));
emptyDirs.add(createDir(namespacedLogsDir1.append("dir_" + i).append("emptyDir1")));
emptyDirs.add(createDir(namespacedLogsDir1.append("dir_" + i).append("emptyDir2")));
emptyDirs.add(createDir(namespacedLogsDir1.append("abc").append("dir_" + i)));
emptyDirs.add(createDir(namespacedLogsDir1.append("abc").append("def").append("dir_" + i)));
emptyDirs.add(createDir(namespacedLogsDir2.append("def").append("dir_" + i)));
emptyDirs.add(createDir(namespacedLogsDir2.append("def").append("hij").append("dir_" + i)));
}
LogCleanup logCleanup = new LogCleanup(null, baseDir, namespacesDir, RETENTION_DURATION_MS);
for (Location location : Sets.newHashSet(Iterables.concat(nonEmptyDirs, emptyDirs))) {
logCleanup.deleteEmptyDir("ns1/" + logBaseDir, location);
logCleanup.deleteEmptyDir("ns2/" + logBaseDir, location);
}
// Assert non-empty dirs (and their files) are still present
for (Location location : files) {
Assert.assertTrue("Location " + location + " is deleted!", location.exists());
}
// Assert empty dirs are deleted
for (Location location : emptyDirs) {
Assert.assertFalse("Dir " + location + " is still present!", location.exists());
}
// Assert base dir and namespaced log dirs exist
Assert.assertTrue(baseDir.exists());
Assert.assertTrue(namespacedLogsDir1.exists());
Assert.assertTrue(namespacedLogsDir2.exists());
}
@Test
public void testDeleteEmptyDir2() throws Exception {
// Create base dir
LocationFactory locationFactory = injector.getInstance(LocationFactory.class);
Location baseDir = locationFactory.create(TEMP_FOLDER.newFolder().toURI());
LogCleanup logCleanup = new LogCleanup(null, baseDir, namespacesDir, RETENTION_DURATION_MS);
logCleanup.deleteEmptyDir(namespacesDir + "/ns/" + logBaseDir, baseDir);
// Assert base dir exists
Assert.assertTrue(baseDir.exists());
baseDir.mkdirs();
// Assert root exists
Assert.assertTrue(baseDir.exists());
logCleanup.deleteEmptyDir(namespacesDir + "/ns/" + logBaseDir, baseDir);
// Assert root still exists
Assert.assertTrue(baseDir.exists());
Location namespaceDir = baseDir.append(namespacesDir).append("ns");
namespaceDir.mkdirs();
Assert.assertTrue(namespaceDir.exists());
logCleanup.deleteEmptyDir("ns/" + logBaseDir, namespaceDir);
// Assert root still exists
Assert.assertTrue(namespaceDir.exists());
Location tmpPath = locationFactory.create("/tmp");
tmpPath.mkdirs();
Assert.assertTrue(tmpPath.exists());
logCleanup.deleteEmptyDir("ns/" + logBaseDir, tmpPath);
// Assert tmp still exists
Assert.assertTrue(tmpPath.exists());
}
private Location createFile(Location path) throws Exception {
Location parent = Locations.getParent(path);
Assert.assertNotNull(parent);
parent.mkdirs();
path.createNew();
Assert.assertTrue(path.exists());
return path;
}
private Location createDir(Location path) throws Exception {
path.mkdirs();
return path;
}
private String locationListsToString(List<Location> list1, List<Location> list2) {
return ImmutableList.of(Lists.transform(list1, LOCATION_URI_FUNCTION),
Lists.transform(list2, LOCATION_URI_FUNCTION)).toString();
}
private static final Function<Location, URI> LOCATION_URI_FUNCTION =
new Function<Location, URI>() {
@Override
public URI apply(Location input) {
return input.toURI();
}
};
}