/*
* 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 org.apache.accumulo.test.functional;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.cli.BatchWriterOpts;
import org.apache.accumulo.core.cli.ScannerOpts;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.Instance;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.ZooKeeperInstance;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.metadata.MetadataTable;
import org.apache.accumulo.core.metadata.schema.MetadataSchema;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.security.TablePermission;
import org.apache.accumulo.core.util.ServerServices;
import org.apache.accumulo.core.util.ServerServices.Service;
import org.apache.accumulo.core.zookeeper.ZooUtil;
import org.apache.accumulo.fate.zookeeper.ZooLock;
import org.apache.accumulo.gc.SimpleGarbageCollector;
import org.apache.accumulo.minicluster.MemoryUnit;
import org.apache.accumulo.minicluster.ServerType;
import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl;
import org.apache.accumulo.minicluster.impl.ProcessNotFoundException;
import org.apache.accumulo.minicluster.impl.ProcessReference;
import org.apache.accumulo.server.zookeeper.ZooReaderWriter;
import org.apache.accumulo.test.TestIngest;
import org.apache.accumulo.test.VerifyIngest;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RawLocalFileSystem;
import org.apache.hadoop.io.Text;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.junit.Assert;
import org.junit.Test;
import com.google.common.collect.Iterators;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
public class GarbageCollectorIT extends ConfigurableMacBase {
private static final String OUR_SECRET = "itsreallysecret";
@Override
public int defaultTimeoutSeconds() {
return 5 * 60;
}
@Override
public void configure(MiniAccumuloConfigImpl cfg, Configuration hadoopCoreSite) {
cfg.setProperty(Property.INSTANCE_ZK_TIMEOUT, "15s");
cfg.setProperty(Property.INSTANCE_SECRET, OUR_SECRET);
cfg.setProperty(Property.GC_CYCLE_START, "1");
cfg.setProperty(Property.GC_CYCLE_DELAY, "1");
cfg.setProperty(Property.GC_PORT, "0");
cfg.setProperty(Property.TSERV_MAXMEM, "5K");
cfg.setProperty(Property.TSERV_MAJC_DELAY, "1");
// use raw local file system so walogs sync and flush will work
hadoopCoreSite.set("fs.file.impl", RawLocalFileSystem.class.getName());
}
private void killMacGc() throws ProcessNotFoundException, InterruptedException, KeeperException {
// kill gc started by MAC
getCluster().killProcess(ServerType.GARBAGE_COLLECTOR, getCluster().getProcesses().get(ServerType.GARBAGE_COLLECTOR).iterator().next());
// delete lock in zookeeper if there, this will allow next GC to start quickly
String path = ZooUtil.getRoot(new ZooKeeperInstance(getCluster().getClientConfig())) + Constants.ZGC_LOCK;
ZooReaderWriter zk = new ZooReaderWriter(cluster.getZooKeepers(), 30000, OUR_SECRET);
try {
ZooLock.deleteLock(zk, path);
} catch (IllegalStateException e) {
}
assertNull(getCluster().getProcesses().get(ServerType.GARBAGE_COLLECTOR));
}
@Test
public void gcTest() throws Exception {
killMacGc();
Connector c = getConnector();
c.tableOperations().create("test_ingest");
c.tableOperations().setProperty("test_ingest", Property.TABLE_SPLIT_THRESHOLD.getKey(), "5K");
TestIngest.Opts opts = new TestIngest.Opts();
VerifyIngest.Opts vopts = new VerifyIngest.Opts();
vopts.rows = opts.rows = 10000;
vopts.cols = opts.cols = 1;
opts.setPrincipal("root");
vopts.setPrincipal("root");
TestIngest.ingest(c, cluster.getFileSystem(), opts, new BatchWriterOpts());
c.tableOperations().compact("test_ingest", null, null, true, true);
int before = countFiles();
while (true) {
sleepUninterruptibly(1, TimeUnit.SECONDS);
int more = countFiles();
if (more <= before)
break;
before = more;
}
// restart GC
getCluster().start();
sleepUninterruptibly(15, TimeUnit.SECONDS);
int after = countFiles();
VerifyIngest.verifyIngest(c, vopts, new ScannerOpts());
assertTrue(after < before);
}
@Test
public void gcLotsOfCandidatesIT() throws Exception {
killMacGc();
log.info("Filling metadata table with bogus delete flags");
Connector c = getConnector();
addEntries(c, new BatchWriterOpts());
cluster.getConfig().setDefaultMemory(10, MemoryUnit.MEGABYTE);
Process gc = cluster.exec(SimpleGarbageCollector.class);
sleepUninterruptibly(20, TimeUnit.SECONDS);
String output = "";
while (!output.contains("delete candidates has exceeded")) {
byte buffer[] = new byte[10 * 1024];
try {
int n = gc.getInputStream().read(buffer);
output = new String(buffer, 0, n, UTF_8);
} catch (IOException ex) {
break;
}
}
gc.destroy();
assertTrue(output.contains("delete candidates has exceeded"));
}
@Test
public void dontGCRootLog() throws Exception {
killMacGc();
// dirty metadata
Connector c = getConnector();
String table = getUniqueNames(1)[0];
c.tableOperations().create(table);
// let gc run for a bit
cluster.start();
sleepUninterruptibly(20, TimeUnit.SECONDS);
killMacGc();
// kill tservers
for (ProcessReference ref : cluster.getProcesses().get(ServerType.TABLET_SERVER)) {
cluster.killProcess(ServerType.TABLET_SERVER, ref);
}
// run recovery
cluster.start();
// did it recover?
Scanner scanner = c.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
Iterators.size(scanner.iterator());
}
private Mutation createDelMutation(String path, String cf, String cq, String val) {
Text row = new Text(MetadataSchema.DeletesSection.getRowPrefix() + path);
Mutation delFlag = new Mutation(row);
delFlag.put(cf, cq, val);
return delFlag;
}
@Test
public void testInvalidDelete() throws Exception {
killMacGc();
String table = getUniqueNames(1)[0];
getConnector().tableOperations().create(table);
BatchWriter bw2 = getConnector().createBatchWriter(table, new BatchWriterConfig());
Mutation m1 = new Mutation("r1");
m1.put("cf1", "cq1", "v1");
bw2.addMutation(m1);
bw2.close();
getConnector().tableOperations().flush(table, null, null, true);
// ensure an invalid delete entry does not cause GC to go berserk ACCUMULO-2520
getConnector().securityOperations().grantTablePermission(getConnector().whoami(), MetadataTable.NAME, TablePermission.WRITE);
BatchWriter bw3 = getConnector().createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
bw3.addMutation(createDelMutation("", "", "", ""));
bw3.addMutation(createDelMutation("", "testDel", "test", "valueTest"));
bw3.addMutation(createDelMutation("/", "", "", ""));
bw3.close();
Process gc = cluster.exec(SimpleGarbageCollector.class);
try {
String output = "";
while (!output.contains("Ignoring invalid deletion candidate")) {
sleepUninterruptibly(250, TimeUnit.MILLISECONDS);
try {
output = FunctionalTestUtils.readAll(cluster, SimpleGarbageCollector.class, gc);
} catch (IOException ioe) {
log.error("Could not read all from cluster.", ioe);
}
}
} finally {
gc.destroy();
}
Scanner scanner = getConnector().createScanner(table, Authorizations.EMPTY);
Iterator<Entry<Key,Value>> iter = scanner.iterator();
assertTrue(iter.hasNext());
Entry<Key,Value> entry = iter.next();
Assert.assertEquals("r1", entry.getKey().getRow().toString());
Assert.assertEquals("cf1", entry.getKey().getColumnFamily().toString());
Assert.assertEquals("cq1", entry.getKey().getColumnQualifier().toString());
Assert.assertEquals("v1", entry.getValue().toString());
Assert.assertFalse(iter.hasNext());
}
@Test
public void testProperPortAdvertisement() throws Exception {
Connector conn = getConnector();
Instance instance = conn.getInstance();
ZooReaderWriter zk = new ZooReaderWriter(cluster.getZooKeepers(), 30000, OUR_SECRET);
String path = ZooUtil.getRoot(instance) + Constants.ZGC_LOCK;
for (int i = 0; i < 5; i++) {
List<String> locks;
try {
locks = zk.getChildren(path, null);
} catch (NoNodeException e) {
Thread.sleep(5000);
continue;
}
if (locks != null && locks.size() > 0) {
Collections.sort(locks);
String lockPath = path + "/" + locks.get(0);
String gcLoc = new String(zk.getData(lockPath, null));
Assert.assertTrue("Found unexpected data in zookeeper for GC location: " + gcLoc, gcLoc.startsWith(Service.GC_CLIENT.name()));
int loc = gcLoc.indexOf(ServerServices.SEPARATOR_CHAR);
Assert.assertNotEquals("Could not find split point of GC location for: " + gcLoc, -1, loc);
String addr = gcLoc.substring(loc + 1);
int addrSplit = addr.indexOf(':');
Assert.assertNotEquals("Could not find split of GC host:port for: " + addr, -1, addrSplit);
String host = addr.substring(0, addrSplit), port = addr.substring(addrSplit + 1);
// We shouldn't have the "bindall" address in zk
Assert.assertNotEquals("0.0.0.0", host);
// Nor should we have the "random port" in zk
Assert.assertNotEquals(0, Integer.parseInt(port));
return;
}
Thread.sleep(5000);
}
Assert.fail("Could not find advertised GC address");
}
private int countFiles() throws Exception {
Path path = new Path(cluster.getConfig().getDir() + "/accumulo/tables/1/*/*.rf");
return Iterators.size(Arrays.asList(cluster.getFileSystem().globStatus(path)).iterator());
}
public static void addEntries(Connector conn, BatchWriterOpts bwOpts) throws Exception {
conn.securityOperations().grantTablePermission(conn.whoami(), MetadataTable.NAME, TablePermission.WRITE);
BatchWriter bw = conn.createBatchWriter(MetadataTable.NAME, bwOpts.getBatchWriterConfig());
for (int i = 0; i < 100000; ++i) {
final Text emptyText = new Text("");
Text row = new Text(String.format("%s/%020d/%s", MetadataSchema.DeletesSection.getRowPrefix(), i,
"aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggghhhhhhhhhhiiiiiiiiiijjjjjjjjjj"));
Mutation delFlag = new Mutation(row);
delFlag.put(emptyText, emptyText, new Value(new byte[] {}));
bw.addMutation(delFlag);
}
bw.close();
}
}