accumulo-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ctubb...@apache.org
Subject [39/54] [partial] ACCUMULO-658, ACCUMULO-656 Split server into separate modules
Date Fri, 01 Nov 2013 00:56:18 GMT
http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/java/org/apache/accumulo/server/util/CloneTest.java
----------------------------------------------------------------------
diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/CloneTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/CloneTest.java
new file mode 100644
index 0000000..e2d1ecb
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/util/CloneTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.server.util;
+
+import java.util.HashSet;
+import java.util.Map.Entry;
+
+import junit.framework.TestCase;
+
+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.Scanner;
+import org.apache.accumulo.core.client.mock.MockInstance;
+import org.apache.accumulo.core.client.security.tokens.PasswordToken;
+import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.KeyExtent;
+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.TabletsSection;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily;
+import org.apache.accumulo.core.security.Authorizations;
+import org.apache.hadoop.io.Text;
+
+public class CloneTest extends TestCase {
+  
+  public void testNoFiles() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    KeyExtent ke = new KeyExtent(new Text("0"), null, null);
+    Mutation mut = ke.getPrevRowUpdateMutation();
+    
+    TabletsSection.ServerColumnFamily.TIME_COLUMN.put(mut, new Value("M0".getBytes()));
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mut, new Value("/default_tablet".getBytes()));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(mut);
+    
+    bw1.close();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    // scan tables metadata entries and confirm the same
+    
+  }
+  
+  public void testFilesChange() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    KeyExtent ke = new KeyExtent(new Text("0"), null, null);
+    Mutation mut = ke.getPrevRowUpdateMutation();
+    
+    TabletsSection.ServerColumnFamily.TIME_COLUMN.put(mut, new Value("M0".getBytes()));
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mut, new Value("/default_tablet".getBytes()));
+    mut.put(DataFileColumnFamily.NAME.toString(), "/default_tablet/0_0.rf", "1,200");
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(mut);
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    Mutation mut2 = new Mutation(ke.getMetadataEntry());
+    mut2.putDelete(DataFileColumnFamily.NAME.toString(), "/default_tablet/0_0.rf");
+    mut2.put(DataFileColumnFamily.NAME.toString(), "/default_tablet/1_0.rf", "2,300");
+    
+    bw1.addMutation(mut2);
+    bw1.flush();
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(1, rc);
+    
+    rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    Scanner scanner = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+    scanner.setRange(new KeyExtent(new Text("1"), null, null).toMetadataRange());
+    
+    HashSet<String> files = new HashSet<String>();
+    
+    for (Entry<Key,Value> entry : scanner) {
+      if (entry.getKey().getColumnFamily().equals(DataFileColumnFamily.NAME))
+        files.add(entry.getKey().getColumnQualifier().toString());
+    }
+    
+    assertEquals(1, files.size());
+    assertTrue(files.contains("../0/default_tablet/1_0.rf"));
+    
+  }
+  
+  // test split where files of children are the same
+  public void testSplit1() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(createTablet("0", null, null, "/default_tablet", "/default_tablet/0_0.rf"));
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    bw1.addMutation(createTablet("0", "m", null, "/default_tablet", "/default_tablet/0_0.rf"));
+    bw1.addMutation(createTablet("0", null, "m", "/t-1", "/default_tablet/0_0.rf"));
+    
+    bw1.flush();
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    Scanner scanner = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+    scanner.setRange(new KeyExtent(new Text("1"), null, null).toMetadataRange());
+    
+    HashSet<String> files = new HashSet<String>();
+    
+    int count = 0;
+    for (Entry<Key,Value> entry : scanner) {
+      if (entry.getKey().getColumnFamily().equals(DataFileColumnFamily.NAME)) {
+        files.add(entry.getKey().getColumnQualifier().toString());
+        count++;
+      }
+    }
+    
+    assertEquals(1, count);
+    assertEquals(1, files.size());
+    assertTrue(files.contains("../0/default_tablet/0_0.rf"));
+  }
+  
+  // test split where files of children differ... like majc and split occurred
+  public void testSplit2() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(createTablet("0", null, null, "/default_tablet", "/default_tablet/0_0.rf"));
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    bw1.addMutation(createTablet("0", "m", null, "/default_tablet", "/default_tablet/1_0.rf"));
+    Mutation mut3 = createTablet("0", null, "m", "/t-1", "/default_tablet/1_0.rf");
+    mut3.putDelete(DataFileColumnFamily.NAME.toString(), "/default_tablet/0_0.rf");
+    bw1.addMutation(mut3);
+    
+    bw1.flush();
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(1, rc);
+    
+    rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    Scanner scanner = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+    scanner.setRange(new KeyExtent(new Text("1"), null, null).toMetadataRange());
+    
+    HashSet<String> files = new HashSet<String>();
+    
+    int count = 0;
+    
+    for (Entry<Key,Value> entry : scanner) {
+      if (entry.getKey().getColumnFamily().equals(DataFileColumnFamily.NAME)) {
+        files.add(entry.getKey().getColumnQualifier().toString());
+        count++;
+      }
+    }
+    
+    assertEquals(1, files.size());
+    assertEquals(2, count);
+    assertTrue(files.contains("../0/default_tablet/1_0.rf"));
+  }
+  
+  private static Mutation deleteTablet(String tid, String endRow, String prevRow, String dir, String file) throws Exception {
+    KeyExtent ke = new KeyExtent(new Text(tid), endRow == null ? null : new Text(endRow), prevRow == null ? null : new Text(prevRow));
+    Mutation mut = new Mutation(ke.getMetadataEntry());
+    TabletsSection.TabletColumnFamily.PREV_ROW_COLUMN.putDelete(mut);
+    TabletsSection.ServerColumnFamily.TIME_COLUMN.putDelete(mut);
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.putDelete(mut);
+    mut.putDelete(DataFileColumnFamily.NAME.toString(), file);
+    
+    return mut;
+  }
+  
+  private static Mutation createTablet(String tid, String endRow, String prevRow, String dir, String file) throws Exception {
+    KeyExtent ke = new KeyExtent(new Text(tid), endRow == null ? null : new Text(endRow), prevRow == null ? null : new Text(prevRow));
+    Mutation mut = ke.getPrevRowUpdateMutation();
+    
+    TabletsSection.ServerColumnFamily.TIME_COLUMN.put(mut, new Value("M0".getBytes()));
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mut, new Value(dir.getBytes()));
+    mut.put(DataFileColumnFamily.NAME.toString(), file, "10,200");
+    
+    return mut;
+  }
+  
+  // test two tablets splitting into four
+  public void testSplit3() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(createTablet("0", "m", null, "/d1", "/d1/file1"));
+    bw1.addMutation(createTablet("0", null, "m", "/d2", "/d2/file2"));
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    bw1.addMutation(createTablet("0", "f", null, "/d1", "/d1/file3"));
+    bw1.addMutation(createTablet("0", "m", "f", "/d3", "/d1/file1"));
+    bw1.addMutation(createTablet("0", "s", "m", "/d2", "/d2/file2"));
+    bw1.addMutation(createTablet("0", null, "s", "/d4", "/d2/file2"));
+    
+    bw1.flush();
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    Scanner scanner = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+    scanner.setRange(new KeyExtent(new Text("1"), null, null).toMetadataRange());
+    
+    HashSet<String> files = new HashSet<String>();
+    
+    int count = 0;
+    for (Entry<Key,Value> entry : scanner) {
+      if (entry.getKey().getColumnFamily().equals(DataFileColumnFamily.NAME)) {
+        files.add(entry.getKey().getColumnQualifier().toString());
+        count++;
+      }
+    }
+    
+    assertEquals(2, count);
+    assertEquals(2, files.size());
+    assertTrue(files.contains("../0/d1/file1"));
+    assertTrue(files.contains("../0/d2/file2"));
+  }
+  
+  // test cloned marker
+  public void testClonedMarker() throws Exception {
+    
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(createTablet("0", "m", null, "/d1", "/d1/file1"));
+    bw1.addMutation(createTablet("0", null, "m", "/d2", "/d2/file2"));
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    bw1.addMutation(deleteTablet("0", "m", null, "/d1", "/d1/file1"));
+    bw1.addMutation(deleteTablet("0", null, "m", "/d2", "/d2/file2"));
+    
+    bw1.flush();
+    
+    bw1.addMutation(createTablet("0", "f", null, "/d1", "/d1/file3"));
+    bw1.addMutation(createTablet("0", "m", "f", "/d3", "/d1/file1"));
+    bw1.addMutation(createTablet("0", "s", "m", "/d2", "/d2/file3"));
+    bw1.addMutation(createTablet("0", null, "s", "/d4", "/d4/file3"));
+    
+    bw1.flush();
+    
+    int rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(1, rc);
+    
+    bw1.addMutation(deleteTablet("0", "m", "f", "/d3", "/d1/file1"));
+    
+    bw1.flush();
+    
+    bw1.addMutation(createTablet("0", "m", "f", "/d3", "/d1/file3"));
+    
+    bw1.flush();
+    
+    rc = MetadataTableUtil.checkClone("0", "1", conn, bw2);
+    
+    assertEquals(0, rc);
+    
+    Scanner scanner = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+    scanner.setRange(new KeyExtent(new Text("1"), null, null).toMetadataRange());
+    
+    HashSet<String> files = new HashSet<String>();
+    
+    int count = 0;
+    for (Entry<Key,Value> entry : scanner) {
+      if (entry.getKey().getColumnFamily().equals(DataFileColumnFamily.NAME)) {
+        files.add(entry.getKey().getColumnQualifier().toString());
+        count++;
+      }
+    }
+    
+    assertEquals(3, count);
+    assertEquals(3, files.size());
+    assertTrue(files.contains("../0/d1/file1"));
+    assertTrue(files.contains("../0/d2/file3"));
+    assertTrue(files.contains("../0/d4/file3"));
+  }
+  
+  // test two tablets splitting into four
+  public void testMerge() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    bw1.addMutation(createTablet("0", "m", null, "/d1", "/d1/file1"));
+    bw1.addMutation(createTablet("0", null, "m", "/d2", "/d2/file2"));
+    
+    bw1.flush();
+    
+    BatchWriter bw2 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    
+    MetadataTableUtil.initializeClone("0", "1", conn, bw2);
+    
+    bw1.addMutation(deleteTablet("0", "m", null, "/d1", "/d1/file1"));
+    Mutation mut = createTablet("0", null, null, "/d2", "/d2/file2");
+    mut.put(DataFileColumnFamily.NAME.toString(), "/d1/file1", "10,200");
+    bw1.addMutation(mut);
+    
+    bw1.flush();
+    
+    try {
+      MetadataTableUtil.checkClone("0", "1", conn, bw2);
+      assertTrue(false);
+    } catch (TabletIterator.TabletDeletedException tde) {}
+    
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/java/org/apache/accumulo/server/util/DefaultMapTest.java
----------------------------------------------------------------------
diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/DefaultMapTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/DefaultMapTest.java
new file mode 100644
index 0000000..b68d412
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/util/DefaultMapTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.server.util;
+
+import static org.junit.Assert.*;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.accumulo.server.util.DefaultMap;
+import org.junit.Test;
+
+public class DefaultMapTest {
+  
+  @Test
+  public void testDefaultMap() {
+    Integer value = new DefaultMap<String,Integer>(0).get("test");
+    assertNotNull(value);
+    assertEquals(new Integer(0), value);
+    value = new DefaultMap<String,Integer>(1).get("test");
+    assertNotNull(value);
+    assertEquals(new Integer(1), value);
+    
+    AtomicInteger canConstruct = new DefaultMap<String,AtomicInteger>(new AtomicInteger(1)).get("test");
+    assertNotNull(canConstruct);
+    assertEquals(new AtomicInteger(0).get(), canConstruct.get());
+    
+    DefaultMap<String,String> map = new DefaultMap<String,String>("");
+    assertEquals(map.get("foo"), "");
+    map.put("foo", "bar");
+    assertEquals(map.get("foo"), "bar");
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/java/org/apache/accumulo/server/util/TabletIteratorTest.java
----------------------------------------------------------------------
diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/TabletIteratorTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/TabletIteratorTest.java
new file mode 100644
index 0000000..72ce334
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/util/TabletIteratorTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.server.util;
+
+import java.util.Map.Entry;
+
+import junit.framework.TestCase;
+
+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.Scanner;
+import org.apache.accumulo.core.client.mock.MockInstance;
+import org.apache.accumulo.core.client.security.tokens.PasswordToken;
+import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.KeyExtent;
+import org.apache.accumulo.core.data.Mutation;
+import org.apache.accumulo.core.data.Range;
+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.metadata.schema.MetadataSchema.TabletsSection;
+import org.apache.accumulo.core.security.Authorizations;
+import org.apache.accumulo.server.util.TabletIterator.TabletDeletedException;
+import org.apache.hadoop.io.Text;
+
+public class TabletIteratorTest extends TestCase {
+  
+  class TestTabletIterator extends TabletIterator {
+    
+    private Connector conn;
+    
+    public TestTabletIterator(Connector conn) throws Exception {
+      super(conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY), MetadataSchema.TabletsSection.getRange(), true, true);
+      this.conn = conn;
+    }
+    
+    @Override
+    protected void resetScanner() {
+      try {
+        Scanner ds = conn.createScanner(MetadataTable.NAME, Authorizations.EMPTY);
+        Text tablet = new KeyExtent(new Text("0"), new Text("m"), null).getMetadataEntry();
+        ds.setRange(new Range(tablet, true, tablet, true));
+        
+        Mutation m = new Mutation(tablet);
+        
+        BatchWriter bw = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+        for (Entry<Key,Value> entry : ds) {
+          Key k = entry.getKey();
+          m.putDelete(k.getColumnFamily(), k.getColumnQualifier(), k.getTimestamp());
+        }
+        
+        bw.addMutation(m);
+        
+        bw.close();
+        
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+      
+      super.resetScanner();
+    }
+    
+  }
+  
+  // simulate a merge happening while iterating over tablets
+  public void testMerge() throws Exception {
+    MockInstance mi = new MockInstance();
+    Connector conn = mi.getConnector("", new PasswordToken(""));
+    
+    KeyExtent ke1 = new KeyExtent(new Text("0"), new Text("m"), null);
+    Mutation mut1 = ke1.getPrevRowUpdateMutation();
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mut1, new Value("/d1".getBytes()));
+    
+    KeyExtent ke2 = new KeyExtent(new Text("0"), null, null);
+    Mutation mut2 = ke2.getPrevRowUpdateMutation();
+    TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mut2, new Value("/d2".getBytes()));
+    
+    BatchWriter bw1 = conn.createBatchWriter(MetadataTable.NAME, new BatchWriterConfig());
+    bw1.addMutation(mut1);
+    bw1.addMutation(mut2);
+    bw1.close();
+    
+    TestTabletIterator tabIter = new TestTabletIterator(conn);
+    
+    try {
+      while (tabIter.hasNext()) {
+        tabIter.next();
+      }
+      assertTrue(false);
+    } catch (TabletDeletedException tde) {}
+  }
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/java/org/apache/accumulo/server/util/time/BaseRelativeTimeTest.java
----------------------------------------------------------------------
diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/time/BaseRelativeTimeTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/time/BaseRelativeTimeTest.java
new file mode 100644
index 0000000..fdedd84
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/util/time/BaseRelativeTimeTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.server.util.time;
+
+import static org.junit.Assert.*;
+
+import org.apache.accumulo.server.util.time.BaseRelativeTime;
+import org.apache.accumulo.server.util.time.ProvidesTime;
+import org.junit.Test;
+
+public class BaseRelativeTimeTest {
+  
+  static class BogusTime implements ProvidesTime {
+    public long value = 0;
+    
+    public long currentTime() {
+      return value;
+    }
+  }
+  
+  @Test
+  public void testMatchesTime() {
+    BogusTime bt = new BogusTime();
+    BogusTime now = new BogusTime();
+    now.value = bt.value = System.currentTimeMillis();
+    
+    BaseRelativeTime brt = new BaseRelativeTime(now);
+    assertEquals(brt.currentTime(), now.value);
+    brt.updateTime(now.value);
+    assertEquals(brt.currentTime(), now.value);
+  }
+  
+  @Test
+  public void testFutureTime() {
+    BogusTime advice = new BogusTime();
+    BogusTime local = new BogusTime();
+    local.value = advice.value = System.currentTimeMillis();
+    // Ten seconds into the future
+    advice.value += 10000;
+    
+    BaseRelativeTime brt = new BaseRelativeTime(local);
+    assertEquals(brt.currentTime(), local.value);
+    brt.updateTime(advice.value);
+    long once = brt.currentTime();
+    assertTrue(once < advice.value);
+    assertTrue(once > local.value);
+    
+    for (int i = 0; i < 100; i++) {
+      brt.updateTime(advice.value);
+    }
+    long many = brt.currentTime();
+    assertTrue(many > once);
+    assertTrue("after much advice, relative time is still closer to local time", (advice.value - many) < (once - local.value));
+  }
+  
+  @Test
+  public void testPastTime() {
+    BogusTime advice = new BogusTime();
+    BogusTime local = new BogusTime();
+    local.value = advice.value = System.currentTimeMillis();
+    // Ten seconds into the past
+    advice.value -= 10000;
+    
+    BaseRelativeTime brt = new BaseRelativeTime(local);
+    brt.updateTime(advice.value);
+    long once = brt.currentTime();
+    assertTrue(once < local.value);
+    brt.updateTime(advice.value);
+    long twice = brt.currentTime();
+    assertTrue("Time cannot go backwards", once <= twice);
+    brt.updateTime(advice.value - 10000);
+    assertTrue("Time cannot go backwards", once <= twice);
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/resources/accumulo-site.xml
----------------------------------------------------------------------
diff --git a/server/base/src/test/resources/accumulo-site.xml b/server/base/src/test/resources/accumulo-site.xml
new file mode 100644
index 0000000..2aa9fff
--- /dev/null
+++ b/server/base/src/test/resources/accumulo-site.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+
+<configuration>
+
+  <property>
+    <name>instance.dfs.dir</name>
+    <value>${project.build.directory}/instanceTest</value>
+  </property>
+
+  <property>
+    <name>instance.secret</name>
+    <value>TEST_SYSTEM_SECRET</value>
+  </property>
+
+</configuration>

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/base/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/server/base/src/test/resources/log4j.properties b/server/base/src/test/resources/log4j.properties
new file mode 100644
index 0000000..3206832
--- /dev/null
+++ b/server/base/src/test/resources/log4j.properties
@@ -0,0 +1,21 @@
+# 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.
+
+log4j.rootLogger=INFO, CA
+log4j.appender.CA=org.apache.log4j.ConsoleAppender
+log4j.appender.CA.layout=org.apache.log4j.PatternLayout
+log4j.appender.CA.layout.ConversionPattern=%d{ISO8601} [%-8c{2}] %-5p: %m%n
+
+log4j.logger.org.apache.accumulo.server.util.TabletIterator=ERROR
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/gc/pom.xml
----------------------------------------------------------------------
diff --git a/server/gc/pom.xml b/server/gc/pom.xml
new file mode 100644
index 0000000..4a79b6f
--- /dev/null
+++ b/server/gc/pom.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.accumulo</groupId>
+    <artifactId>accumulo-project</artifactId>
+    <version>1.6.0-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+  <artifactId>accumulo-gc</artifactId>
+  <name>GC Server</name>
+  <dependencies>
+    <dependency>
+      <groupId>com.beust</groupId>
+      <artifactId>jcommander</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>jline</groupId>
+      <artifactId>jline</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-fate</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-server-base</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-start</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-trace</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.thrift</groupId>
+      <artifactId>libthrift</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-collections</groupId>
+      <artifactId>commons-collections</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-configuration</groupId>
+      <artifactId>commons-configuration</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-lang</groupId>
+      <artifactId>commons-lang</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-client</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.zookeeper</groupId>
+      <artifactId>zookeeper</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectWriteAheadLogs.java
----------------------------------------------------------------------
diff --git a/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectWriteAheadLogs.java b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectWriteAheadLogs.java
new file mode 100644
index 0000000..776d68a
--- /dev/null
+++ b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectWriteAheadLogs.java
@@ -0,0 +1,309 @@
+/*
+ * 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.gc;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.client.Instance;
+import org.apache.accumulo.core.conf.AccumuloConfiguration;
+import org.apache.accumulo.core.gc.thrift.GCStatus;
+import org.apache.accumulo.core.gc.thrift.GcCycleStats;
+import org.apache.accumulo.core.tabletserver.thrift.TabletClientService;
+import org.apache.accumulo.core.tabletserver.thrift.TabletClientService.Client;
+import org.apache.accumulo.core.util.AddressUtil;
+import org.apache.accumulo.core.util.ThriftUtil;
+import org.apache.accumulo.core.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerConstants;
+import org.apache.accumulo.server.fs.VolumeManager;
+import org.apache.accumulo.server.fs.VolumeManager.FileType;
+import org.apache.accumulo.server.security.SystemCredentials;
+import org.apache.accumulo.server.util.MetadataTableUtil;
+import org.apache.accumulo.server.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.trace.instrument.Span;
+import org.apache.accumulo.trace.instrument.Trace;
+import org.apache.accumulo.trace.instrument.Tracer;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.Path;
+import org.apache.log4j.Logger;
+import org.apache.thrift.TException;
+import org.apache.zookeeper.KeeperException;
+
+import com.google.common.net.HostAndPort;
+
+public class GarbageCollectWriteAheadLogs {
+  private static final Logger log = Logger.getLogger(GarbageCollectWriteAheadLogs.class);
+  
+  private final Instance instance;
+  private final VolumeManager fs;
+  
+  private boolean useTrash;
+  
+  GarbageCollectWriteAheadLogs(Instance instance, VolumeManager fs, boolean useTrash) throws IOException {
+    this.instance = instance;
+    this.fs = fs;
+  }
+  
+  public void collect(GCStatus status) {
+    
+    Span span = Trace.start("scanServers");
+    try {
+      
+      Set<Path> sortedWALogs = getSortedWALogs();
+      
+      status.currentLog.started = System.currentTimeMillis();
+      
+      Map<Path,String> fileToServerMap = new HashMap<Path,String>();
+      int count = scanServers(fileToServerMap);
+      long fileScanStop = System.currentTimeMillis();
+      log.info(String.format("Fetched %d files from %d servers in %.2f seconds", fileToServerMap.size(), count,
+          (fileScanStop - status.currentLog.started) / 1000.));
+      status.currentLog.candidates = fileToServerMap.size();
+      span.stop();
+      
+      span = Trace.start("removeMetadataEntries");
+      try {
+        count = removeMetadataEntries(fileToServerMap, sortedWALogs, status);
+      } catch (Exception ex) {
+        log.error("Unable to scan metadata table", ex);
+        return;
+      } finally {
+        span.stop();
+      }
+      
+      long logEntryScanStop = System.currentTimeMillis();
+      log.info(String.format("%d log entries scanned in %.2f seconds", count, (logEntryScanStop - fileScanStop) / 1000.));
+      
+      span = Trace.start("removeFiles");
+      Map<String,ArrayList<Path>> serverToFileMap = mapServersToFiles(fileToServerMap);
+      
+      count = removeFiles(serverToFileMap, sortedWALogs, status);
+      
+      long removeStop = System.currentTimeMillis();
+      log.info(String.format("%d total logs removed from %d servers in %.2f seconds", count, serverToFileMap.size(), (removeStop - logEntryScanStop) / 1000.));
+      status.currentLog.finished = removeStop;
+      status.lastLog = status.currentLog;
+      status.currentLog = new GcCycleStats();
+      span.stop();
+      
+    } catch (Exception e) {
+      log.error("exception occured while garbage collecting write ahead logs", e);
+    } finally {
+      span.stop();
+    }
+  }
+  
+  boolean holdsLock(HostAndPort addr) {
+    try {
+      String zpath = ZooUtil.getRoot(instance) + Constants.ZTSERVERS + "/" + addr.toString();
+      List<String> children = ZooReaderWriter.getInstance().getChildren(zpath);
+      return !(children == null || children.isEmpty());
+    } catch (KeeperException.NoNodeException ex) {
+      return false;
+    } catch (Exception ex) {
+      log.debug(ex, ex);
+      return true;
+    }
+  }
+  
+  private int removeFiles(Map<String,ArrayList<Path>> serverToFileMap, Set<Path> sortedWALogs, final GCStatus status) {
+    AccumuloConfiguration conf = instance.getConfiguration();
+    for (Entry<String,ArrayList<Path>> entry : serverToFileMap.entrySet()) {
+      if (entry.getKey().isEmpty()) {
+        // old-style log entry, just remove it
+        for (Path path : entry.getValue()) {
+          log.debug("Removing old-style WAL " + path);
+          try {
+            if (!useTrash || !fs.moveToTrash(path))
+              fs.deleteRecursively(path);
+            status.currentLog.deleted++;
+          } catch (FileNotFoundException ex) {
+            // ignored
+          } catch (IOException ex) {
+            log.error("Unable to delete wal " + path + ": " + ex);
+          }
+        }
+      } else {
+        HostAndPort address = AddressUtil.parseAddress(entry.getKey());
+        if (!holdsLock(address)) {
+          for (Path path : entry.getValue()) {
+            log.debug("Removing WAL for offline server " + path);
+            try {
+              if (!useTrash || !fs.moveToTrash(path))
+                fs.deleteRecursively(path);
+              status.currentLog.deleted++;
+            } catch (FileNotFoundException ex) {
+              // ignored
+            } catch (IOException ex) {
+              log.error("Unable to delete wal " + path + ": " + ex);
+            }
+          }
+          continue;
+        } else {
+          Client tserver = null;
+          try {
+            tserver = ThriftUtil.getClient(new TabletClientService.Client.Factory(), address, conf);
+            tserver.removeLogs(Tracer.traceInfo(), SystemCredentials.get().toThrift(instance), paths2strings(entry.getValue()));
+            log.debug("deleted " + entry.getValue() + " from " + entry.getKey());
+            status.currentLog.deleted += entry.getValue().size();
+          } catch (TException e) {
+            log.warn("Error talking to " + address + ": " + e);
+          } finally {
+            if (tserver != null)
+              ThriftUtil.returnClient(tserver);
+          }
+        }
+      }
+    }
+    
+    for (Path swalog : sortedWALogs) {
+      log.debug("Removing sorted WAL " + swalog);
+      try {
+        if (!useTrash || !fs.moveToTrash(swalog)) {
+          fs.deleteRecursively(swalog);
+        }
+      } catch (FileNotFoundException ex) {
+        // ignored
+      } catch (IOException ioe) {
+        try {
+          if (fs.exists(swalog)) {
+            log.error("Unable to delete sorted walog " + swalog + ": " + ioe);
+          }
+        } catch (IOException ex) {
+          log.error("Unable to check for the existence of " + swalog, ex);
+        }
+      }
+    }
+    
+    return 0;
+  }
+  
+  private List<String> paths2strings(ArrayList<Path> paths) {
+    List<String> result = new ArrayList<String>(paths.size());
+    for (Path path : paths)
+      result.add(path.toString());
+    return result;
+  }
+  
+  private static Map<String,ArrayList<Path>> mapServersToFiles(Map<Path,String> fileToServerMap) {
+    Map<String,ArrayList<Path>> result = new HashMap<String,ArrayList<Path>>();
+    for (Entry<Path,String> fileServer : fileToServerMap.entrySet()) {
+      ArrayList<Path> files = result.get(fileServer.getValue());
+      if (files == null) {
+        files = new ArrayList<Path>();
+        result.put(fileServer.getValue(), files);
+      }
+      files.add(fileServer.getKey());
+    }
+    return result;
+  }
+  
+  private int removeMetadataEntries(Map<Path,String> fileToServerMap, Set<Path> sortedWALogs, GCStatus status) throws IOException, KeeperException,
+      InterruptedException {
+    int count = 0;
+    Iterator<MetadataTableUtil.LogEntry> iterator = MetadataTableUtil.getLogEntries(SystemCredentials.get());
+    while (iterator.hasNext()) {
+      for (String entry : iterator.next().logSet) {
+        String parts[] = entry.split("/", 2);
+        String filename = parts[1];
+        Path path;
+        if (filename.contains(":"))
+          path = new Path(filename);
+        else
+          path = fs.getFullPath(FileType.WAL, filename);
+        
+        if (fileToServerMap.remove(path) != null)
+          status.currentLog.inUse++;
+        
+        sortedWALogs.remove(path);
+        
+        count++;
+      }
+    }
+    return count;
+  }
+
+  //TODO Remove deprecation warning suppression when Hadoop1 support is dropped
+  @SuppressWarnings("deprecation")
+  private int scanServers(Map<Path,String> fileToServerMap) throws Exception {
+    Set<String> servers = new HashSet<String>();
+    for (String walDir : ServerConstants.getWalDirs()) {
+      Path walRoot = new Path(walDir);
+      FileStatus[] listing = fs.listStatus(walRoot);
+      if (listing == null)
+        continue;
+      for (FileStatus status : listing) {
+        String server = status.getPath().getName();
+        servers.add(server);
+        if (status.isDir()) {
+          for (FileStatus file : fs.listStatus(new Path(walRoot, server))) {
+            if (isUUID(file.getPath().getName()))
+              fileToServerMap.put(file.getPath(), server);
+            else {
+              log.info("Ignoring file " + file.getPath() + " because it doesn't look like a uuid");
+            }
+          }
+        } else if (isUUID(server)) {
+          // old-style WAL are not under a directory
+          fileToServerMap.put(status.getPath(), "");
+        } else {
+          log.info("Ignoring file " + status.getPath() + " because it doesn't look like a uuid");
+        }
+      }
+    }
+    return servers.size();
+  }
+  
+  private Set<Path> getSortedWALogs() throws IOException {
+    Set<Path> result = new HashSet<Path>();
+    
+    for (String dir : ServerConstants.getRecoveryDirs()) {
+      Path recoveryDir = new Path(dir);
+      
+      if (fs.exists(recoveryDir)) {
+        for (FileStatus status : fs.listStatus(recoveryDir)) {
+          if (isUUID(status.getPath().getName())) {
+            result.add(status.getPath());
+          } else {
+            log.debug("Ignoring file " + status.getPath() + " because it doesn't look like a uuid");
+          }
+        }
+      }
+    }
+    return result;
+  }
+  
+  static private boolean isUUID(String name) {
+    try {
+      UUID.fromString(name);
+      return true;
+    } catch (IllegalArgumentException ex) {
+      return false;
+    }
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionAlgorithm.java
----------------------------------------------------------------------
diff --git a/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionAlgorithm.java b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionAlgorithm.java
new file mode 100644
index 0000000..464d0d9
--- /dev/null
+++ b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionAlgorithm.java
@@ -0,0 +1,287 @@
+/*
+ * 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.gc;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.client.AccumuloException;
+import org.apache.accumulo.core.client.AccumuloSecurityException;
+import org.apache.accumulo.core.client.TableNotFoundException;
+import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.KeyExtent;
+import org.apache.accumulo.core.data.Value;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ScanFileColumnFamily;
+import org.apache.accumulo.server.ServerConstants;
+import org.apache.accumulo.trace.instrument.Span;
+import org.apache.accumulo.trace.instrument.Trace;
+import org.apache.hadoop.io.Text;
+import org.apache.log4j.Logger;
+
+/**
+ * 
+ */
+public class GarbageCollectionAlgorithm {
+
+  private static final Logger log = Logger.getLogger(GarbageCollectionAlgorithm.class);
+
+  private String makeRelative(String path, int expectedLen) {
+    String relPath = path;
+
+    if (relPath.startsWith("../"))
+      relPath = relPath.substring(3);
+
+    while (relPath.endsWith("/"))
+      relPath = relPath.substring(0, relPath.length() - 1);
+
+    while (relPath.startsWith("/"))
+      relPath = relPath.substring(1);
+
+    String[] tokens = relPath.split("/");
+
+    // handle paths like a//b///c
+    boolean containsEmpty = false;
+    for (String token : tokens) {
+      if (token.equals("")) {
+        containsEmpty = true;
+        break;
+      }
+    }
+
+    if (containsEmpty) {
+      ArrayList<String> tmp = new ArrayList<String>();
+      for (String token : tokens) {
+        if (!token.equals("")) {
+          tmp.add(token);
+        }
+      }
+
+      tokens = tmp.toArray(new String[tmp.size()]);
+    }
+
+    if (tokens.length > 3) {
+      if (!path.contains(":"))
+        throw new IllegalArgumentException(path);
+
+      if (tokens[tokens.length - 4].equals(ServerConstants.TABLE_DIR) && (expectedLen == 0 || expectedLen == 3)) {
+        relPath = tokens[tokens.length - 3] + "/" + tokens[tokens.length - 2] + "/" + tokens[tokens.length - 1];
+      } else if (tokens[tokens.length - 3].equals(ServerConstants.TABLE_DIR) && (expectedLen == 0 || expectedLen == 2)) {
+        relPath = tokens[tokens.length - 2] + "/" + tokens[tokens.length - 1];
+      } else {
+        throw new IllegalArgumentException(path);
+      }
+    } else if (tokens.length == 3 && (expectedLen == 0 || expectedLen == 3)) {
+      relPath = tokens[0] + "/" + tokens[1] + "/" + tokens[2];
+    } else if (tokens.length == 2 && (expectedLen == 0 || expectedLen == 2)) {
+      relPath = tokens[0] + "/" + tokens[1];
+    } else {
+      throw new IllegalArgumentException(path);
+    }
+
+    return relPath;
+  }
+
+  private SortedMap<String,String> makeRelative(Collection<String> candidates) {
+
+    SortedMap<String,String> ret = new TreeMap<String,String>();
+
+    for (String candidate : candidates) {
+      String relPath = makeRelative(candidate, 0);
+      ret.put(relPath, candidate);
+    }
+
+    return ret;
+  }
+
+  private void confirmDeletes(GarbageCollectionEnvironment gce, SortedMap<String,String> candidateMap) throws TableNotFoundException, AccumuloException,
+      AccumuloSecurityException {
+    boolean checkForBulkProcessingFiles = false;
+    Iterator<String> relativePaths = candidateMap.keySet().iterator();
+    while (!checkForBulkProcessingFiles && relativePaths.hasNext())
+      checkForBulkProcessingFiles |= relativePaths.next().toLowerCase(Locale.ENGLISH).contains(Constants.BULK_PREFIX);
+
+    if (checkForBulkProcessingFiles) {
+      Iterator<String> blipiter = gce.getBlipIterator();
+
+      // WARNING: This block is IMPORTANT
+      // You MUST REMOVE candidates that are in the same folder as a bulk
+      // processing flag!
+
+      while (blipiter.hasNext()) {
+        String blipPath = blipiter.next();
+        blipPath = makeRelative(blipPath, 2);
+
+        Iterator<String> tailIter = candidateMap.tailMap(blipPath).keySet().iterator();
+
+        int count = 0;
+
+        while (tailIter.hasNext()) {
+          if (tailIter.next().startsWith(blipPath)) {
+            count++;
+            tailIter.remove();
+          } else {
+            break;
+          }
+        }
+
+        if (count > 0)
+          log.debug("Folder has bulk processing flag: " + blipPath);
+      }
+
+    }
+
+    Iterator<Entry<Key,Value>> iter = gce.getReferenceIterator();
+    while (iter.hasNext()) {
+      Entry<Key,Value> entry = iter.next();
+      Key key = entry.getKey();
+      Text cft = key.getColumnFamily();
+
+      if (cft.equals(DataFileColumnFamily.NAME) || cft.equals(ScanFileColumnFamily.NAME)) {
+        String cq = key.getColumnQualifier().toString();
+
+        String reference = cq;
+        if (cq.startsWith("/")) {
+          String tableID = new String(KeyExtent.tableOfMetadataRow(key.getRow()));
+          reference = "/" + tableID + cq;
+        } else if (!cq.contains(":") && !cq.startsWith("../")) {
+          throw new RuntimeException("Bad file reference " + cq);
+        }
+
+        reference = makeRelative(reference, 3);
+
+        // WARNING: This line is EXTREMELY IMPORTANT.
+        // You MUST REMOVE candidates that are still in use
+        if (candidateMap.remove(reference) != null)
+          log.debug("Candidate was still in use: " + reference);
+
+        String dir = reference.substring(0, reference.lastIndexOf('/'));
+        if (candidateMap.remove(dir) != null)
+          log.debug("Candidate was still in use: " + reference);
+
+      } else if (TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.hasColumns(key)) {
+        String tableID = new String(KeyExtent.tableOfMetadataRow(key.getRow()));
+        String dir = entry.getValue().toString();
+        if (!dir.contains(":")) {
+          if (!dir.startsWith("/"))
+            throw new RuntimeException("Bad directory " + dir);
+          dir = "/" + tableID + dir;
+        }
+
+        dir = makeRelative(dir, 2);
+
+        if (candidateMap.remove(dir) != null)
+          log.debug("Candidate was still in use: " + dir);
+      } else
+        throw new RuntimeException("Scanner over metadata table returned unexpected column : " + entry.getKey());
+    }
+  }
+
+  private void cleanUpDeletedTableDirs(GarbageCollectionEnvironment gce, SortedMap<String,String> candidateMap) throws IOException {
+    HashSet<String> tableIdsWithDeletes = new HashSet<String>();
+
+    // find the table ids that had dirs deleted
+    for (String delete : candidateMap.keySet()) {
+      String[] tokens = delete.split("/");
+      if (tokens.length == 2) {
+        // its a directory
+        String tableId = delete.split("/")[0];
+        tableIdsWithDeletes.add(tableId);
+      }
+    }
+
+    Set<String> tableIdsInZookeeper = gce.getTableIDs();
+
+    tableIdsWithDeletes.removeAll(tableIdsInZookeeper);
+
+    // tableIdsWithDeletes should now contain the set of deleted tables that had dirs deleted
+
+    for (String delTableId : tableIdsWithDeletes) {
+      gce.deleteTableDirIfEmpty(delTableId);
+    }
+
+  }
+
+  private List<String> getCandidates(GarbageCollectionEnvironment gce, String lastCandidate) throws TableNotFoundException, AccumuloException,
+      AccumuloSecurityException {
+    Span candidatesSpan = Trace.start("getCandidates");
+    List<String> candidates;
+    try {
+      candidates = gce.getCandidates(lastCandidate);
+    } finally {
+      candidatesSpan.stop();
+    }
+    return candidates;
+  }
+
+  private void confirmDeletesTrace(GarbageCollectionEnvironment gce, SortedMap<String,String> candidateMap) throws TableNotFoundException, AccumuloException,
+      AccumuloSecurityException {
+    Span confirmDeletesSpan = Trace.start("confirmDeletes");
+    try {
+      confirmDeletes(gce, candidateMap);
+    } finally {
+      confirmDeletesSpan.stop();
+    }
+  }
+
+  private void deleteConfirmed(GarbageCollectionEnvironment gce, SortedMap<String,String> candidateMap) throws IOException, AccumuloException,
+      AccumuloSecurityException, TableNotFoundException {
+    Span deleteSpan = Trace.start("deleteFiles");
+    try {
+      gce.delete(candidateMap);
+    } finally {
+      deleteSpan.stop();
+    }
+
+    cleanUpDeletedTableDirs(gce, candidateMap);
+  }
+
+  public void collect(GarbageCollectionEnvironment gce) throws TableNotFoundException, AccumuloException, AccumuloSecurityException, IOException {
+
+    String lastCandidate = "";
+
+    while (true) {
+      List<String> candidates = getCandidates(gce, lastCandidate);
+
+      if (candidates.size() == 0)
+        break;
+      else
+        lastCandidate = candidates.get(candidates.size() - 1);
+
+      long origSize = candidates.size();
+      gce.incrementCandidatesStat(origSize);
+
+      SortedMap<String,String> candidateMap = makeRelative(candidates);
+
+      confirmDeletesTrace(gce, candidateMap);
+      gce.incrementInUseStat(origSize - candidateMap.size());
+
+      deleteConfirmed(gce, candidateMap);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionEnvironment.java
----------------------------------------------------------------------
diff --git a/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionEnvironment.java b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionEnvironment.java
new file mode 100644
index 0000000..3e36617
--- /dev/null
+++ b/server/gc/src/main/java/org/apache/accumulo/gc/GarbageCollectionEnvironment.java
@@ -0,0 +1,118 @@
+/*
+ * 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.gc;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+
+import org.apache.accumulo.core.client.AccumuloException;
+import org.apache.accumulo.core.client.AccumuloSecurityException;
+import org.apache.accumulo.core.client.TableNotFoundException;
+import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.Value;
+import org.apache.accumulo.core.metadata.MetadataTable;
+import org.apache.accumulo.core.metadata.RootTable;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ScanFileColumnFamily;
+
+/**
+ * 
+ */
+public interface GarbageCollectionEnvironment {
+
+  /**
+   * Return a list of paths to files and dirs which are candidates for deletion from a given table, {@link RootTable#NAME} or {@link MetadataTable#NAME}
+   * 
+   * @param continuePoint
+   *          A row to resume from if a previous invocation was stopped due to finding an extremely large number of candidates to remove which would have
+   *          exceeded memory limitations
+   * @return A collection of candidates files for deletion, may not be the complete collection of files for deletion at this point in time
+   * @throws TableNotFoundException
+   * @throws AccumuloException
+   * @throws AccumuloSecurityException
+   */
+  List<String> getCandidates(String continuePoint) throws TableNotFoundException, AccumuloException, AccumuloSecurityException;
+
+  /**
+   * Fetch a list of paths for all bulk loads in progress (blip) from a given table, {@link RootTable#NAME} or {@link MetadataTable#NAME}
+   * 
+   * @return The list of files for each bulk load currently in progress.
+   * @throws TableNotFoundException
+   * @throws AccumuloException
+   * @throws AccumuloSecurityException
+   */
+  Iterator<String> getBlipIterator() throws TableNotFoundException, AccumuloException, AccumuloSecurityException;
+
+  /**
+   * Fetches the references to files, {@link DataFileColumnFamily#NAME} or {@link ScanFileColumnFamily#NAME}, from tablets
+   * 
+   * @return An {@link Iterator} of {@link Entry}&lt;{@link Key}, {@link Value}&gt; which constitute a reference to a file.
+   * @throws TableNotFoundException
+   * @throws AccumuloException
+   * @throws AccumuloSecurityException
+   */
+  Iterator<Entry<Key,Value>> getReferenceIterator() throws TableNotFoundException, AccumuloException, AccumuloSecurityException;
+
+  /**
+   * Return the set of tableIDs for the given instance this GarbageCollector is running over
+   * 
+   * @return The valueSet for the table name to table id map.
+   */
+  Set<String> getTableIDs();
+
+  /**
+   * Delete the given files from the provided {@link Map} of relative path to absolute path for each file that should be deleted
+   * 
+   * @param candidateMap
+   *          A Map from relative path to absolute path for files to be deleted.
+   * @throws IOException
+   * @throws AccumuloSecurityException
+   * @throws AccumuloException
+   * @throws TableNotFoundException
+   */
+  void delete(SortedMap<String,String> candidateMap) throws IOException, AccumuloException, AccumuloSecurityException, TableNotFoundException;
+
+  /**
+   * Delete a table's directory if it is empty.
+   * 
+   * @param tableID
+   *          The id of the table whose directory we are to operate on
+   * @throws IOException
+   */
+  void deleteTableDirIfEmpty(String tableID) throws IOException;
+
+  /**
+   * Increment the number of candidates for deletion for the current garbage collection run
+   * 
+   * @param i
+   *          Value to increment the deletion candidates by
+   */
+  void incrementCandidatesStat(long i);
+
+  /**
+   * Increment the number of files still in use for the current garbage collection run
+   * 
+   * @param i
+   *          Value to increment the still-in-use count by.
+   */
+  void incrementInUseStat(long i);
+}

http://git-wip-us.apache.org/repos/asf/accumulo/blob/598821cd/server/gc/src/main/java/org/apache/accumulo/gc/SimpleGarbageCollector.java
----------------------------------------------------------------------
diff --git a/server/gc/src/main/java/org/apache/accumulo/gc/SimpleGarbageCollector.java b/server/gc/src/main/java/org/apache/accumulo/gc/SimpleGarbageCollector.java
new file mode 100644
index 0000000..fdd08f4
--- /dev/null
+++ b/server/gc/src/main/java/org/apache/accumulo/gc/SimpleGarbageCollector.java
@@ -0,0 +1,578 @@
+/*
+ * 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.gc;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.client.AccumuloException;
+import org.apache.accumulo.core.client.AccumuloSecurityException;
+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.IsolatedScanner;
+import org.apache.accumulo.core.client.MutationsRejectedException;
+import org.apache.accumulo.core.client.Scanner;
+import org.apache.accumulo.core.client.TableNotFoundException;
+import org.apache.accumulo.core.client.impl.Tables;
+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.PartialKey;
+import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.Value;
+import org.apache.accumulo.core.gc.thrift.GCMonitorService.Iface;
+import org.apache.accumulo.core.gc.thrift.GCMonitorService.Processor;
+import org.apache.accumulo.core.gc.thrift.GCStatus;
+import org.apache.accumulo.core.gc.thrift.GcCycleStats;
+import org.apache.accumulo.core.master.state.tables.TableState;
+import org.apache.accumulo.core.metadata.MetadataTable;
+import org.apache.accumulo.core.metadata.RootTable;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ScanFileColumnFamily;
+import org.apache.accumulo.core.security.Authorizations;
+import org.apache.accumulo.core.security.Credentials;
+import org.apache.accumulo.core.security.SecurityUtil;
+import org.apache.accumulo.core.security.thrift.TCredentials;
+import org.apache.accumulo.core.util.NamingThreadFactory;
+import org.apache.accumulo.core.util.ServerServices;
+import org.apache.accumulo.core.util.ServerServices.Service;
+import org.apache.accumulo.core.util.UtilWaitThread;
+import org.apache.accumulo.core.zookeeper.ZooUtil;
+import org.apache.accumulo.fate.zookeeper.ZooLock.LockLossReason;
+import org.apache.accumulo.fate.zookeeper.ZooLock.LockWatcher;
+import org.apache.accumulo.server.Accumulo;
+import org.apache.accumulo.server.ServerConstants;
+import org.apache.accumulo.server.ServerOpts;
+import org.apache.accumulo.server.client.HdfsZooInstance;
+import org.apache.accumulo.server.conf.ServerConfiguration;
+import org.apache.accumulo.server.fs.VolumeManager;
+import org.apache.accumulo.server.fs.VolumeManager.FileType;
+import org.apache.accumulo.server.fs.VolumeManagerImpl;
+import org.apache.accumulo.server.security.SystemCredentials;
+import org.apache.accumulo.server.tables.TableManager;
+import org.apache.accumulo.server.util.Halt;
+import org.apache.accumulo.server.util.TServerUtils;
+import org.apache.accumulo.server.util.TabletIterator;
+import org.apache.accumulo.server.zookeeper.ZooLock;
+import org.apache.accumulo.trace.instrument.CountSampler;
+import org.apache.accumulo.trace.instrument.Sampler;
+import org.apache.accumulo.trace.instrument.Span;
+import org.apache.accumulo.trace.instrument.Trace;
+import org.apache.accumulo.trace.instrument.thrift.TraceWrap;
+import org.apache.accumulo.trace.thrift.TInfo;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.io.Text;
+import org.apache.log4j.Logger;
+import org.apache.zookeeper.KeeperException;
+
+import com.beust.jcommander.Parameter;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterators;
+import com.google.common.net.HostAndPort;
+
+public class SimpleGarbageCollector implements Iface {
+  private static final Text EMPTY_TEXT = new Text();
+  
+  static class Opts extends ServerOpts {
+    @Parameter(names = {"-v", "--verbose"}, description = "extra information will get printed to stdout also")
+    boolean verbose = false;
+    @Parameter(names = {"-s", "--safemode"}, description = "safe mode will not delete files")
+    boolean safeMode = false;
+  }
+  
+  // how much of the JVM's available memory should it use gathering candidates
+  private static final float CANDIDATE_MEMORY_PERCENTAGE = 0.75f;
+
+  private static final Logger log = Logger.getLogger(SimpleGarbageCollector.class);
+  
+  private Credentials credentials;
+  private long gcStartDelay;
+  private VolumeManager fs;
+  private boolean useTrash = true;
+  private Opts opts = new Opts();
+  private ZooLock lock;
+  
+  private GCStatus status = new GCStatus(new GcCycleStats(), new GcCycleStats(), new GcCycleStats(), new GcCycleStats());
+  
+  private int numDeleteThreads;
+  
+  private Instance instance;
+  
+  public static void main(String[] args) throws UnknownHostException, IOException {
+    SecurityUtil.serverLogin();
+    Instance instance = HdfsZooInstance.getInstance();
+    ServerConfiguration serverConf = new ServerConfiguration(instance);
+    final VolumeManager fs = VolumeManagerImpl.get();
+    Accumulo.init(fs, serverConf, "gc");
+    Opts opts = new Opts();
+    opts.parseArgs("gc", args);
+    SimpleGarbageCollector gc = new SimpleGarbageCollector(opts);
+    
+    gc.init(fs, instance, SystemCredentials.get(), serverConf.getConfiguration().getBoolean(Property.GC_TRASH_IGNORE));
+    Accumulo.enableTracing(opts.getAddress(), "gc");
+    gc.run();
+  }
+  
+  public SimpleGarbageCollector(Opts opts) {
+    this.opts = opts;
+  }
+  
+  public void init(VolumeManager fs, Instance instance, Credentials credentials, boolean noTrash) throws IOException {
+    this.fs = fs;
+    this.credentials = credentials;
+    this.instance = instance;
+    
+    gcStartDelay = instance.getConfiguration().getTimeInMillis(Property.GC_CYCLE_START);
+    long gcDelay = instance.getConfiguration().getTimeInMillis(Property.GC_CYCLE_DELAY);
+    numDeleteThreads = instance.getConfiguration().getCount(Property.GC_DELETE_THREADS);
+    log.info("start delay: " + gcStartDelay + " milliseconds");
+    log.info("time delay: " + gcDelay + " milliseconds");
+    log.info("safemode: " + opts.safeMode);
+    log.info("verbose: " + opts.verbose);
+    log.info("memory threshold: " + CANDIDATE_MEMORY_PERCENTAGE + " of " + Runtime.getRuntime().maxMemory() + " bytes");
+    log.info("delete threads: " + numDeleteThreads);
+    useTrash = !noTrash;
+  }
+  
+  private class GCEnv implements GarbageCollectionEnvironment {
+
+    private String tableName;
+
+    GCEnv(String tableName) {
+      this.tableName = tableName;
+    }
+
+    @Override
+    public List<String> getCandidates(String continuePoint) throws TableNotFoundException, AccumuloException, AccumuloSecurityException {
+      // want to ensure GC makes progress... if the 1st N deletes are stable and we keep processing them,
+      // then will never inspect deletes after N
+      Range range = MetadataSchema.DeletesSection.getRange();
+      if (continuePoint != null && !continuePoint.isEmpty()) {
+        String continueRow = MetadataSchema.DeletesSection.getRowPrefix() + continuePoint;
+        range = new Range(new Key(continueRow).followingKey(PartialKey.ROW), true, range.getEndKey(), range.isEndKeyInclusive());
+      }
+
+      Scanner scanner = instance.getConnector(credentials.getPrincipal(), credentials.getToken()).createScanner(tableName, Authorizations.EMPTY);
+      scanner.setRange(range);
+      List<String> result = new ArrayList<String>();
+      // find candidates for deletion; chop off the prefix
+      for (Entry<Key,Value> entry : scanner) {
+        String cand = entry.getKey().getRow().toString().substring(MetadataSchema.DeletesSection.getRowPrefix().length());
+        result.add(cand);
+        if (almostOutOfMemory()) {
+          log.info("List of delete candidates has exceeded the memory threshold. Attempting to delete what has been gathered so far.");
+          break;
+        }
+      }
+
+      return result;
+
+    }
+
+    @Override
+    public Iterator<String> getBlipIterator() throws TableNotFoundException, AccumuloException, AccumuloSecurityException {
+      IsolatedScanner scanner = new IsolatedScanner(instance.getConnector(credentials.getPrincipal(), credentials.getToken()).createScanner(tableName,
+          Authorizations.EMPTY));
+
+      scanner.setRange(MetadataSchema.BlipSection.getRange());
+
+      return Iterators.transform(scanner.iterator(), new Function<Entry<Key,Value>,String>() {
+        @Override
+        public String apply(Entry<Key,Value> entry) {
+          return entry.getKey().getRow().toString().substring(MetadataSchema.BlipSection.getRowPrefix().length());
+        }
+      });
+    }
+
+    @Override
+    public Iterator<Entry<Key,Value>> getReferenceIterator() throws TableNotFoundException, AccumuloException, AccumuloSecurityException {
+      IsolatedScanner scanner = new IsolatedScanner(instance.getConnector(credentials.getPrincipal(), credentials.getToken()).createScanner(tableName,
+          Authorizations.EMPTY));
+      scanner.fetchColumnFamily(DataFileColumnFamily.NAME);
+      scanner.fetchColumnFamily(ScanFileColumnFamily.NAME);
+      TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.fetch(scanner);
+      TabletIterator tabletIterator = new TabletIterator(scanner, MetadataSchema.TabletsSection.getRange(), false, true);
+
+      return Iterators.concat(Iterators.transform(tabletIterator, new Function<Map<Key,Value>,Iterator<Entry<Key,Value>>>() {
+        @Override
+        public Iterator<Entry<Key,Value>> apply(Map<Key,Value> input) {
+          return input.entrySet().iterator();
+        }
+      }));
+    }
+
+    @Override
+    public Set<String> getTableIDs() {
+      return Tables.getIdToNameMap(instance).keySet();
+    }
+
+    @Override
+    public void delete(SortedMap<String,String> confirmedDeletes) throws IOException, AccumuloException, AccumuloSecurityException, TableNotFoundException {
+
+      if (opts.safeMode) {
+        if (opts.verbose)
+          System.out.println("SAFEMODE: There are " + confirmedDeletes.size() + " data file candidates marked for deletion.%n"
+              + "          Examine the log files to identify them.%n");
+        log.info("SAFEMODE: Listing all data file candidates for deletion");
+        for (String s : confirmedDeletes.values())
+          log.info("SAFEMODE: " + s);
+        log.info("SAFEMODE: End candidates for deletion");
+        return;
+      }
+
+      Connector c = instance.getConnector(SystemCredentials.get().getPrincipal(), SystemCredentials.get().getToken());
+      BatchWriter writer = c.createBatchWriter(tableName, new BatchWriterConfig());
+
+      // when deleting a dir and all files in that dir, only need to delete the dir
+      // the dir will sort right before the files... so remove the files in this case
+      // to minimize namenode ops
+      Iterator<Entry<String,String>> cdIter = confirmedDeletes.entrySet().iterator();
+
+      String lastDir = null;
+      while (cdIter.hasNext()) {
+        Entry<String,String> entry = cdIter.next();
+        String relPath = entry.getKey();
+        String absPath = fs.getFullPath(FileType.TABLE, entry.getValue()).toString();
+
+        if (isDir(relPath)) {
+          lastDir = absPath;
+        } else if (lastDir != null) {
+          if (absPath.startsWith(lastDir)) {
+            log.debug("Ignoring " + entry.getValue() + " because " + lastDir + " exist");
+            try {
+              putMarkerDeleteMutation(entry.getValue(), writer);
+            } catch (MutationsRejectedException e) {
+              throw new RuntimeException(e);
+            }
+            cdIter.remove();
+          } else {
+            lastDir = null;
+          }
+        }
+      }
+
+      final BatchWriter finalWriter = writer;
+
+      ExecutorService deleteThreadPool = Executors.newFixedThreadPool(numDeleteThreads, new NamingThreadFactory("deleting"));
+
+      for (final String delete : confirmedDeletes.values()) {
+
+        Runnable deleteTask = new Runnable() {
+          @Override
+          public void run() {
+            boolean removeFlag;
+
+            try {
+              Path fullPath = fs.getFullPath(FileType.TABLE, delete);
+
+              log.debug("Deleting " + fullPath);
+
+              if (moveToTrash(fullPath) || fs.deleteRecursively(fullPath)) {
+                // delete succeeded, still want to delete
+                removeFlag = true;
+                synchronized (SimpleGarbageCollector.this) {
+                  ++status.current.deleted;
+                }
+              } else if (fs.exists(fullPath)) {
+                // leave the entry in the METADATA table; we'll try again
+                // later
+                removeFlag = false;
+                synchronized (SimpleGarbageCollector.this) {
+                  ++status.current.errors;
+                }
+                log.warn("File exists, but was not deleted for an unknown reason: " + fullPath);
+              } else {
+                // this failure, we still want to remove the METADATA table
+                // entry
+                removeFlag = true;
+                synchronized (SimpleGarbageCollector.this) {
+                  ++status.current.errors;
+                }
+                String parts[] = delete.split("/");
+                if (parts.length > 2) {
+                  String tableId = parts[parts.length - 3];
+                  String tabletDir = parts[parts.length - 2];
+                  TableManager.getInstance().updateTableStateCache(tableId);
+                  TableState tableState = TableManager.getInstance().getTableState(tableId);
+                  if (tableState != null && tableState != TableState.DELETING) {
+                    // clone directories don't always exist
+                    if (!tabletDir.startsWith("c-"))
+                      log.warn("File doesn't exist: " + fullPath);
+                  }
+                } else {
+                  log.warn("Very strange path name: " + delete);
+                }
+              }
+
+              // proceed to clearing out the flags for successful deletes and
+              // non-existent files
+              if (removeFlag && finalWriter != null) {
+                putMarkerDeleteMutation(delete, finalWriter);
+              }
+            } catch (Exception e) {
+              log.error(e, e);
+            }
+
+          }
+
+        };
+
+        deleteThreadPool.execute(deleteTask);
+      }
+
+      deleteThreadPool.shutdown();
+
+      try {
+        while (!deleteThreadPool.awaitTermination(1000, TimeUnit.MILLISECONDS)) {}
+      } catch (InterruptedException e1) {
+        log.error(e1, e1);
+      }
+
+      if (writer != null) {
+        try {
+          writer.close();
+        } catch (MutationsRejectedException e) {
+          log.error("Problem removing entries from the metadata table: ", e);
+        }
+      }
+    }
+
+    @Override
+    public void deleteTableDirIfEmpty(String tableID) throws IOException {
+      // if dir exist and is empty, then empty list is returned...
+      // hadoop 1.0 will return null if the file doesn't exist
+      // hadoop 2.0 will throw an exception if the file does not exist
+      for (String dir : ServerConstants.getTablesDirs()) {
+        FileStatus[] tabletDirs = null;
+        try {
+          tabletDirs = fs.listStatus(new Path(dir + "/" + tableID));
+        } catch (FileNotFoundException ex) {
+          // ignored
+        }
+        if (tabletDirs == null)
+          continue;
+
+        if (tabletDirs.length == 0) {
+          Path p = new Path(dir + "/" + tableID);
+          log.debug("Removing table dir " + p);
+          if (!moveToTrash(p))
+            fs.delete(p);
+        }
+      }
+    }
+
+    @Override
+    public void incrementCandidatesStat(long i) {
+      status.current.candidates += i;
+    }
+
+    @Override
+    public void incrementInUseStat(long i) {
+      status.current.inUse += i;
+    }
+
+  }
+
+  private void run() {
+    long tStart, tStop;
+    
+    // Sleep for an initial period, giving the master time to start up and
+    // old data files to be unused
+      
+    try {
+      getZooLock(startStatsService());
+    } catch (Exception ex) {
+      log.error(ex, ex);
+      System.exit(1);
+    }
+
+    try {
+      log.debug("Sleeping for " + gcStartDelay + " milliseconds before beginning garbage collection cycles");
+      Thread.sleep(gcStartDelay);
+    } catch (InterruptedException e) {
+      log.warn(e, e);
+      return;
+    }
+    
+    Sampler sampler = new CountSampler(100);
+    
+    while (true) {
+      if (sampler.next())
+        Trace.on("gc");
+      
+      Span gcSpan = Trace.start("loop");
+      tStart = System.currentTimeMillis();
+      try {
+        System.gc(); // make room
+
+        status.current.started = System.currentTimeMillis();
+
+        new GarbageCollectionAlgorithm().collect(new GCEnv(RootTable.NAME));
+        new GarbageCollectionAlgorithm().collect(new GCEnv(MetadataTable.NAME));
+
+        log.info("Number of data file candidates for deletion: " + status.current.candidates);
+        log.info("Number of data file candidates still in use: " + status.current.inUse);
+        log.info("Number of successfully deleted data files: " + status.current.deleted);
+        log.info("Number of data files delete failures: " + status.current.errors);
+
+        status.current.finished = System.currentTimeMillis();
+        status.last = status.current;
+        status.current = new GcCycleStats();
+        
+      } catch (Exception e) {
+        log.error(e, e);
+      }
+
+      tStop = System.currentTimeMillis();
+      log.info(String.format("Collect cycle took %.2f seconds", ((tStop - tStart) / 1000.0)));
+      
+      // Clean up any unused write-ahead logs
+      Span waLogs = Trace.start("walogs");
+      try {
+        GarbageCollectWriteAheadLogs walogCollector = new GarbageCollectWriteAheadLogs(instance, fs, useTrash);
+        log.info("Beginning garbage collection of write-ahead logs");
+        walogCollector.collect(status);
+      } catch (Exception e) {
+        log.error(e, e);
+      } finally {
+        waLogs.stop();
+      }
+      gcSpan.stop();
+      
+      // we just made a lot of changes to the !METADATA table: flush them out
+      try {
+        Connector connector = instance.getConnector(credentials.getPrincipal(), credentials.getToken());
+        connector.tableOperations().compact(MetadataTable.NAME, null, null, true, true);
+        connector.tableOperations().compact(RootTable.NAME, null, null, true, true);
+      } catch (Exception e) {
+        log.warn(e, e);
+      }
+      
+      Trace.offNoFlush();
+      try {
+        long gcDelay = instance.getConfiguration().getTimeInMillis(Property.GC_CYCLE_DELAY);
+        log.debug("Sleeping for " + gcDelay + " milliseconds");
+        Thread.sleep(gcDelay);
+      } catch (InterruptedException e) {
+        log.warn(e, e);
+        return;
+      }
+    }
+  }
+  
+  private boolean moveToTrash(Path path) throws IOException {
+    if (!useTrash)
+      return false;
+    try {
+      return fs.moveToTrash(path);
+    } catch (FileNotFoundException ex) {
+      return false;
+    }
+  }
+  
+  private void getZooLock(HostAndPort addr) throws KeeperException, InterruptedException {
+    String path = ZooUtil.getRoot(HdfsZooInstance.getInstance()) + Constants.ZGC_LOCK;
+    
+    LockWatcher lockWatcher = new LockWatcher() {
+      @Override
+      public void lostLock(LockLossReason reason) {
+        Halt.halt("GC lock in zookeeper lost (reason = " + reason + "), exiting!");
+      }
+      
+      @Override
+      public void unableToMonitorLockNode(final Throwable e) {
+        Halt.halt(-1, new Runnable() {
+          
+          @Override
+          public void run() {
+            log.fatal("No longer able to monitor lock node ", e);
+          }
+        });
+        
+      }
+    };
+    
+    while (true) {
+      lock = new ZooLock(path);
+      if (lock.tryLock(lockWatcher, new ServerServices(addr.toString(), Service.GC_CLIENT).toString().getBytes())) {
+        break;
+      }
+      UtilWaitThread.sleep(1000);
+    }
+  }
+  
+  private HostAndPort startStatsService() throws UnknownHostException {
+    Processor<Iface> processor = new Processor<Iface>(TraceWrap.service(this));
+    int port = instance.getConfiguration().getPort(Property.GC_PORT);
+    long maxMessageSize = instance.getConfiguration().getMemoryInBytes(Property.GENERAL_MAX_MESSAGE_SIZE);
+    HostAndPort result = HostAndPort.fromParts(opts.getAddress(), port);
+    try {
+      port = TServerUtils.startTServer(result, processor, this.getClass().getSimpleName(), "GC Monitor Service", 2, 1000, maxMessageSize).address.getPort();
+    } catch (Exception ex) {
+      log.fatal(ex, ex);
+      throw new RuntimeException(ex);
+    }
+    return result;
+  }
+  
+
+  static public boolean almostOutOfMemory() {
+    Runtime runtime = Runtime.getRuntime();
+    return runtime.totalMemory() - runtime.freeMemory() > CANDIDATE_MEMORY_PERCENTAGE * runtime.maxMemory();
+  }
+  
+  
+  final static String METADATA_TABLE_DIR = "/" + MetadataTable.ID;
+  
+  private static void putMarkerDeleteMutation(final String delete, final BatchWriter writer)
+      throws MutationsRejectedException {
+    Mutation m = new Mutation(MetadataSchema.DeletesSection.getRowPrefix() + delete);
+    m.putDelete(EMPTY_TEXT, EMPTY_TEXT);
+    writer.addMutation(m);
+  }
+  
+  
+  private boolean isDir(String delete) {
+    int slashCount = 0;
+    for (int i = 0; i < delete.length(); i++)
+      if (delete.charAt(i) == '/')
+        slashCount++;
+    return slashCount == 1;
+  }
+  
+  @Override
+  public GCStatus getStatus(TInfo info, TCredentials credentials) {
+    return status;
+  }
+}


Mime
View raw message