This is an automated email from the ASF dual-hosted git repository.

mmiller pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/main by this push:
     new e12fcbefb6 Refactor Root Tablet serialization code (#2718)
e12fcbefb6 is described below

commit e12fcbefb6f2979c3aeabd5597f3799c107df1ba
Author: Mike Miller <mmil...@apache.org>
AuthorDate: Fri May 27 11:26:59 2022 +0000

    Refactor Root Tablet serialization code (#2718)
    
    * Remove static state from RootTabletMetadata and RootGcCandidates
    * Replaced static from() methods in RootTabletMetadata and RootGcCandidates 
with constructors
    * Added comment to AccumuloDataVersion javadoc
    * Store entries natively in the simpler string-based multi-map, rather
      than converting to key/value pairs and back again. Only convert to
      key/value pairs when needed.
    * Get rid of `new GsonBuilder().create()`. Even Gson's own javadoc
      recommends `new Gson()` if all you want is the defaults.
    
    Co-authored-by: Christopher Tubbs <ctubb...@apache.org>
---
 .../core/clientImpl/bulk/BulkSerialize.java        |   3 +-
 .../schema/ExternalCompactionFinalState.java       |   3 +-
 .../schema/ExternalCompactionMetadata.java         |   3 +-
 .../core/metadata/schema/RootTabletMetadata.java   | 224 +++++++++------------
 .../core/metadata/schema/TabletMetadata.java       |   2 +-
 .../core/metadata/schema/TabletsMetadata.java      |   9 +-
 .../accumulo/server/AccumuloDataVersion.java       |   2 +-
 .../accumulo/server/init/ZooKeeperInitializer.java |  32 ++-
 .../accumulo/server/metadata/RootGcCandidates.java | 106 +++++-----
 .../server/metadata/RootTabletMutatorImpl.java     |   2 +-
 .../accumulo/server/metadata/ServerAmpleImpl.java  |  17 +-
 .../manager/state/RootTabletStateStoreTest.java    |  11 +-
 .../accumulo/test/CountNameNodeOpsBulkIT.java      |   3 +-
 13 files changed, 198 insertions(+), 219 deletions(-)

diff --git 
a/core/src/main/java/org/apache/accumulo/core/clientImpl/bulk/BulkSerialize.java
 
b/core/src/main/java/org/apache/accumulo/core/clientImpl/bulk/BulkSerialize.java
index 82fb70bc1b..f20efaa9ef 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/clientImpl/bulk/BulkSerialize.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/clientImpl/bulk/BulkSerialize.java
@@ -130,8 +130,7 @@ public class BulkSerialize {
     final Path renamingFile = new Path(bulkDir, Constants.BULK_RENAME_FILE);
     try (OutputStream fsOut = output.create(renamingFile);
         BufferedWriter writer = new BufferedWriter(new 
OutputStreamWriter(fsOut))) {
-      Gson gson = new GsonBuilder().create();
-      gson.toJson(oldToNewNameMap, writer);
+      new Gson().toJson(oldToNewNameMap, writer);
     }
   }
 
diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionFinalState.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionFinalState.java
index 4a01d246a9..6e2aaa15ed 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionFinalState.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionFinalState.java
@@ -27,11 +27,10 @@ import org.apache.hadoop.io.Text;
 
 import com.google.common.base.Preconditions;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 public class ExternalCompactionFinalState {
 
-  private static final Gson GSON = new GsonBuilder().create();
+  private static final Gson GSON = new Gson();
 
   public enum FinalState {
     FINISHED, FAILED
diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java
index b4a11c8084..fcd6bf2c3f 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java
@@ -33,11 +33,10 @@ import 
org.apache.accumulo.core.util.compaction.CompactionExecutorIdImpl;
 import org.apache.hadoop.fs.Path;
 
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 public class ExternalCompactionMetadata {
 
-  private static final Gson GSON = new GsonBuilder().create();
+  private static final Gson GSON = new Gson();
 
   private final Set<StoredTabletFile> jobFiles;
   private final Set<StoredTabletFile> nextFiles;
diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/RootTabletMetadata.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/RootTabletMetadata.java
index c62b13145d..bbd85b6305 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/RootTabletMetadata.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/RootTabletMetadata.java
@@ -18,183 +18,153 @@
  */
 package org.apache.accumulo.core.metadata.schema;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import java.util.Arrays;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.EnumSet;
-import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.TreeMap;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 
-import org.apache.accumulo.core.client.admin.TimeType;
-import org.apache.accumulo.core.data.ArrayByteSequence;
-import org.apache.accumulo.core.data.ByteSequence;
-import org.apache.accumulo.core.data.ColumnUpdate;
 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.RootTable;
 import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.CurrentLocationColumnFamily;
-import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily;
 import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.FutureLocationColumnFamily;
-import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ServerColumnFamily;
-import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.TabletColumnFamily;
-import org.apache.accumulo.core.metadata.schema.TabletMetadata.ColumnType;
 import org.apache.hadoop.io.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Preconditions;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 /**
- * Serializes the root tablet metadata as Json using Accumulo's standard 
metadata table schema.
+ * This class is used to serialize and deserialize root tablet metadata using 
GSon. The only data
+ * stored about the Root Table is the COLUMN_FAMILY, COLUMN_QUALIFIER and 
VALUE.
+ *
+ * @since 2.1.0
  */
 public class RootTabletMetadata {
 
-  private static final Gson GSON = new GsonBuilder().create();
-
-  private static final ByteSequence CURR_LOC_FAM =
-      new ArrayByteSequence(CurrentLocationColumnFamily.STR_NAME);
-  private static final ByteSequence FUTURE_LOC_FAM =
-      new ArrayByteSequence(FutureLocationColumnFamily.STR_NAME);
+  private static final Logger log = 
LoggerFactory.getLogger(RootTabletMetadata.class);
+  private static final CharsetDecoder UTF8_error_detecting_decoder = 
UTF_8.newDecoder();
+  private static final Predicate<Entry<String,TreeMap<String,String>>> 
isLocationCF = e -> {
+    String fam = e.getKey();
+    return fam.equals(CurrentLocationColumnFamily.STR_NAME)
+        || fam.equals(FutureLocationColumnFamily.STR_NAME);
+  };
 
-  private TreeMap<Key,Value> entries = new TreeMap<>();
+  // JSON Mapping Version 1. Released with Accumulo version 2.1.0
+  private static final int VERSION = 1;
 
   // This class is used to serialize and deserialize root tablet metadata 
using GSon. Any changes to
   // this class must consider persisted data.
-  private static class GSonData {
-    int version = 1;
+  private static class Data {
+    private final int version;
+
+    /*
+     * The data is mapped using Strings as follows:
+     *
+     * TreeMap<column_family, TreeMap<column_qualifier, value>>
+     */
+    private final TreeMap<String,TreeMap<String,String>> columnValues;
+
+    public Data(int version, TreeMap<String,TreeMap<String,String>> 
columnValues) {
+      this.version = version;
+      this.columnValues = columnValues;
+    }
+  }
 
-    // Map<column_family, Map<column_qualifier, value>>
-    Map<String,Map<String,String>> columnValues;
+  /**
+   * The expectation is that all data stored in the root tablet can be 
converted to UTF8. This
+   * method checks to ensure the byte sequence can be converted from byte[] to 
UTF8 to byte[] w/o
+   * data corruption. Not all byte arrays can be converted to UTF8.
+   */
+  private static String bytesToUtf8(byte[] byteSequence) {
+    try {
+      return 
UTF8_error_detecting_decoder.decode(ByteBuffer.wrap(byteSequence)).toString();
+    } catch (CharacterCodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  private final Gson gson = new Gson();
+  private final Data data;
+
+  public RootTabletMetadata(String json) {
+    log.debug("Creating root tablet metadata from stored JSON: {}", json);
+    this.data = gson.fromJson(json, Data.class);
+    checkArgument(data.version == VERSION, "Invalid Root Table Metadata JSON 
version %s",
+        data.version);
+    data.columnValues.forEach((fam, qualVals) -> {
+      checkArgument(!fam.isBlank(), "Blank column family in %s", 
data.columnValues);
+      checkArgument(!qualVals.isEmpty(), "No columns in family %s", fam);
+    });
+  }
+
+  public RootTabletMetadata() {
+    this.data = new Data(VERSION, new TreeMap<>());
   }
 
   /**
-   * Apply a metadata table mutation to update internal json.
+   * Apply a metadata table mutation to update internal entries.
    */
   public void update(Mutation m) {
-    Preconditions.checkArgument(new 
Text(m.getRow()).equals(RootTable.EXTENT.toMetaRow()));
+    checkArgument(new Text(m.getRow()).equals(RootTable.EXTENT.toMetaRow()),
+        "Invalid Root Table Row " + new Text(m.getRow()));
 
     m.getUpdates().forEach(cup -> {
-      Preconditions.checkArgument(!cup.hasTimestamp());
-      Preconditions.checkArgument(cup.getColumnVisibility().length == 0);
+      checkArgument(!cup.hasTimestamp(), "Root Table timestamp must be 
empty.");
+      checkArgument(cup.getColumnVisibility().length == 0, "Root Table 
visibility must be empty.");
     });
 
-    for (ColumnUpdate cup : m.getUpdates()) {
-      Key newKey = new Key(m.getRow(), cup.getColumnFamily(), 
cup.getColumnQualifier(),
-          cup.getColumnVisibility(), 1, false, false);
-
+    m.getUpdates().forEach(cup -> {
+      String fam = bytesToUtf8(cup.getColumnFamily());
+      String qual = bytesToUtf8(cup.getColumnQualifier());
+      String val = bytesToUtf8(cup.getValue());
       if (cup.isDeleted()) {
-        entries.remove(newKey);
+        data.columnValues.computeIfPresent(fam, (key, qualVals) -> {
+          qualVals.remove(qual);
+          return qualVals.isEmpty() ? null : qualVals;
+        });
       } else {
-        entries.put(newKey, new Value(cup.getValue()));
+        data.columnValues.computeIfAbsent(fam, k -> new TreeMap<>()).put(qual, 
val);
       }
-    }
+    });
 
     // Ensure there is ever only one location
-    long locsSeen = entries.keySet().stream().map(Key::getColumnFamilyData)
-        .filter(fam -> fam.equals(CURR_LOC_FAM) || 
fam.equals(FUTURE_LOC_FAM)).count();
-
-    if (locsSeen > 1) {
+    if 
(data.columnValues.entrySet().stream().filter(isLocationCF).map(Entry::getValue)
+        .mapToInt(TreeMap::size).sum() > 1) {
       throw new IllegalStateException(
-          "After mutation, root tablet has multiple locations : " + m + " " + 
entries);
-    }
-
-  }
-
-  /**
-   * Convert json to tablet metadata. *
-   */
-  public TabletMetadata convertToTabletMetadata() {
-    return TabletMetadata.convertRow(entries.entrySet().iterator(), 
EnumSet.allOf(ColumnType.class),
-        false);
-  }
-
-  private static String bs2Str(byte[] bs) {
-    String str = new String(bs, UTF_8);
-
-    // The expectation is that all data stored in the root tablet can be 
converted to UTF8. This is
-    // a sanity check to ensure the byte sequence can be converted from byte[] 
to UTF8 to byte[] w/o
-    // data corruption. Not all byte arrays can be converted to UTF8.
-    Preconditions.checkArgument(Arrays.equals(bs, str.getBytes(UTF_8)),
-        "Unsuccessful conversion of %s to utf8", str);
-
-    return str;
-  }
-
-  /**
-   * @return a json representation of this object, use {@link 
#fromJson(String)} to convert the json
-   *         back to an object.
-   */
-  public String toJson() {
-    GSonData gd = new GSonData();
-    gd.columnValues = new TreeMap<>();
-
-    Set<Entry<Key,Value>> es = entries.entrySet();
-    for (Entry<Key,Value> entry : es) {
-      String fam = bs2Str(entry.getKey().getColumnFamilyData().toArray());
-      String qual = bs2Str(entry.getKey().getColumnQualifierData().toArray());
-      String val = bs2Str(entry.getValue().get());
-
-      gd.columnValues.computeIfAbsent(fam, k -> new TreeMap<>()).put(qual, 
val);
+          "After mutation, root tablet has multiple locations : " + m + " " + 
data.columnValues);
     }
-
-    return GSON.toJson(gd);
   }
 
   /**
-   * Converts created by calling {@link #toJson()} back to an object.
+   * Convert this class to a {@link TabletMetadata}
    */
-  public static RootTabletMetadata fromJson(String json) {
-    GSonData gd = GSON.fromJson(json, GSonData.class);
-
-    Preconditions.checkArgument(gd.version == 1);
-
+  public TabletMetadata toTabletMetadata() {
     String row = RootTable.EXTENT.toMetaRow().toString();
-
-    TreeMap<Key,Value> entries = new TreeMap<>();
-
-    gd.columnValues.forEach((fam, qualVals) -> {
-      qualVals.forEach((qual, val) -> {
-        Key k = new Key(row, fam, qual, 1);
-        Value v = new Value(val);
-
-        entries.put(k, v);
-      });
-    });
-
-    RootTabletMetadata rtm = new RootTabletMetadata();
-    rtm.entries = entries;
-
-    return rtm;
+    // use a stream so we don't have to re-sort in a new TreeMap<Key,Value> 
structure
+    Stream<SimpleImmutableEntry<Key,Value>> entries = 
data.columnValues.entrySet().stream()
+        .flatMap(famToQualVal -> famToQualVal.getValue().entrySet().stream()
+            .map(qualVal -> new SimpleImmutableEntry<>(
+                new Key(row, famToQualVal.getKey(), qualVal.getKey(), 1),
+                new Value(qualVal.getValue()))));
+    return TabletMetadata.convertRow(entries.iterator(),
+        EnumSet.allOf(TabletMetadata.ColumnType.class), false);
   }
 
   /**
-   * Converts created by calling {@link #toJson()} back to an object. Assumes 
the json is UTF8
-   * encoded.
+   * @return a JSON representation of the root tablet's data.
    */
-  public static RootTabletMetadata fromJson(byte[] bs) {
-    return fromJson(new String(bs, UTF_8));
+  public String toJson() {
+    return gson.toJson(data);
   }
 
-  /**
-   * Generate initial json for the root tablet metadata.
-   */
-  public static byte[] getInitialJson(String dirName, String file) {
-    ServerColumnFamily.validateDirCol(dirName);
-    Mutation mutation = 
TabletColumnFamily.createPrevRowMutation(RootTable.EXTENT);
-    ServerColumnFamily.DIRECTORY_COLUMN.put(mutation, new Value(dirName));
-
-    mutation.put(DataFileColumnFamily.STR_NAME, file, new DataFileValue(0, 
0).encodeAsValue());
-
-    ServerColumnFamily.TIME_COLUMN.put(mutation,
-        new Value(new MetadataTime(0, TimeType.LOGICAL).encode()));
-
-    RootTabletMetadata rtm = new RootTabletMetadata();
-
-    rtm.update(mutation);
-
-    return rtm.toJson().getBytes(UTF_8);
-  }
 }
diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMetadata.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMetadata.java
index fc98313a67..376fabf236 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMetadata.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMetadata.java
@@ -296,7 +296,7 @@ public class TabletMetadata {
   }
 
   @VisibleForTesting
-  public static TabletMetadata convertRow(Iterator<Entry<Key,Value>> rowIter,
+  public static <E extends Entry<Key,Value>> TabletMetadata 
convertRow(Iterator<E> rowIter,
       EnumSet<ColumnType> fetchedColumns, boolean buildKeyValueMap) {
     Objects.requireNonNull(rowIter);
 
diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletsMetadata.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletsMetadata.java
index 86ae72a075..ff32a55dc4 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletsMetadata.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletsMetadata.java
@@ -19,6 +19,7 @@
 package org.apache.accumulo.core.metadata.schema;
 
 import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 import static 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ServerColumnFamily.COMPACT_COLUMN;
@@ -512,8 +513,8 @@ public class TabletsMetadata implements 
Iterable<TabletMetadata>, AutoCloseable
       case IMMEDIATE:
         ZooReader zooReader = ctx.getZooReader();
         try {
-          return RootTabletMetadata.fromJson(zooReader.getData(zkRoot + 
RootTable.ZROOT_TABLET))
-              .convertToTabletMetadata();
+          byte[] bytes = zooReader.getData(zkRoot + RootTable.ZROOT_TABLET);
+          return new RootTabletMetadata(new String(bytes, 
UTF_8)).toTabletMetadata();
         } catch (InterruptedException | KeeperException e) {
           throw new RuntimeException(e);
         }
@@ -523,8 +524,8 @@ public class TabletsMetadata implements 
Iterable<TabletMetadata>, AutoCloseable
   }
 
   public static TabletMetadata getRootMetadata(String zkRoot, ZooCache zc) {
-    return RootTabletMetadata.fromJson(zc.get(zkRoot + RootTable.ZROOT_TABLET))
-        .convertToTabletMetadata();
+    byte[] jsonBytes = zc.get(zkRoot + RootTable.ZROOT_TABLET);
+    return new RootTabletMetadata(new String(jsonBytes, 
UTF_8)).toTabletMetadata();
   }
 
   private final AutoCloseable closeable;
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/AccumuloDataVersion.java 
b/server/base/src/main/java/org/apache/accumulo/server/AccumuloDataVersion.java
index ad617b8e25..035c59e57c 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/AccumuloDataVersion.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/AccumuloDataVersion.java
@@ -38,7 +38,7 @@ public class AccumuloDataVersion {
 
   /**
    * version (10) reflects changes to how root tablet metadata is serialized 
in zookeeper starting
-   * with 2.1
+   * with 2.1. See {@link 
org.apache.accumulo.core.metadata.schema.RootTabletMetadata}.
    */
   public static final int ROOT_TABLET_META_CHANGES = 10;
 
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
 
b/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
index 71d910b6a4..2dbd617c23 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
@@ -24,12 +24,18 @@ import static 
org.apache.accumulo.server.init.Initialize.REPL_TABLE_ID;
 import java.io.IOException;
 
 import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.client.admin.TimeType;
 import org.apache.accumulo.core.clientImpl.Namespace;
 import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.Mutation;
+import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.manager.state.tables.TableState;
 import org.apache.accumulo.core.manager.thrift.ManagerGoalState;
 import org.apache.accumulo.core.metadata.MetadataTable;
 import org.apache.accumulo.core.metadata.RootTable;
+import org.apache.accumulo.core.metadata.schema.DataFileValue;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema;
+import org.apache.accumulo.core.metadata.schema.MetadataTime;
 import org.apache.accumulo.core.metadata.schema.RootTabletMetadata;
 import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
 import org.apache.accumulo.fate.zookeeper.ZooUtil;
@@ -43,7 +49,7 @@ import org.apache.accumulo.server.tables.TableManager;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.ZooDefs;
 
-class ZooKeeperInitializer {
+public class ZooKeeperInitializer {
 
   private final byte[] EMPTY_BYTE_ARRAY = new byte[0];
   private final byte[] ZERO_CHAR_ARRAY = {'0'};
@@ -130,7 +136,7 @@ class ZooKeeperInitializer {
     zoo.putPersistentData(zkInstanceRoot + Constants.ZPROBLEMS, 
EMPTY_BYTE_ARRAY,
         ZooUtil.NodeExistsPolicy.FAIL);
     zoo.putPersistentData(zkInstanceRoot + RootTable.ZROOT_TABLET,
-        RootTabletMetadata.getInitialJson(rootTabletDirName, 
rootTabletFileUri),
+        getInitialRootTabletJson(rootTabletDirName, rootTabletFileUri),
         ZooUtil.NodeExistsPolicy.FAIL);
     zoo.putPersistentData(zkInstanceRoot + 
RootTable.ZROOT_TABLET_GC_CANDIDATES,
         new RootGcCandidates().toJson().getBytes(UTF_8), 
ZooUtil.NodeExistsPolicy.FAIL);
@@ -176,4 +182,26 @@ class ZooKeeperInitializer {
 
   }
 
+  /**
+   * Generate initial json for the root tablet metadata. Return the JSON 
converted to a byte[].
+   */
+  public static byte[] getInitialRootTabletJson(String dirName, String file) {
+    MetadataSchema.TabletsSection.ServerColumnFamily.validateDirCol(dirName);
+    Mutation mutation =
+        
MetadataSchema.TabletsSection.TabletColumnFamily.createPrevRowMutation(RootTable.EXTENT);
+    
MetadataSchema.TabletsSection.ServerColumnFamily.DIRECTORY_COLUMN.put(mutation,
+        new Value(dirName));
+
+    mutation.put(MetadataSchema.TabletsSection.DataFileColumnFamily.STR_NAME, 
file,
+        new DataFileValue(0, 0).encodeAsValue());
+
+    MetadataSchema.TabletsSection.ServerColumnFamily.TIME_COLUMN.put(mutation,
+        new Value(new MetadataTime(0, TimeType.LOGICAL).encode()));
+
+    RootTabletMetadata rootTabletJson = new RootTabletMetadata();
+    rootTabletJson.update(mutation);
+
+    return rootTabletJson.toJson().getBytes(UTF_8);
+  }
+
 }
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/RootGcCandidates.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/RootGcCandidates.java
index 50a465fa53..5b24808f04 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/RootGcCandidates.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/RootGcCandidates.java
@@ -18,10 +18,8 @@
  */
 package org.apache.accumulo.server.metadata;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.common.base.Preconditions.checkArgument;
 
-import java.util.Collection;
-import java.util.Iterator;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
@@ -31,85 +29,73 @@ import java.util.stream.Stream;
 import org.apache.accumulo.core.metadata.StoredTabletFile;
 import org.apache.hadoop.fs.Path;
 
-import com.google.common.base.Preconditions;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 public class RootGcCandidates {
-  private static final Gson GSON = new GsonBuilder().create();
+  // Version 1. Released with Accumulo version 2.1.0
+  private static final int VERSION = 1;
+
+  private final Gson gson = new Gson();
+  private final Data data;
 
   // This class is used to serialize and deserialize root tablet metadata 
using GSon. Any changes to
   // this class must consider persisted data.
-  private static class GSonData {
-    int version = 1;
-
-    // SortedMap<dir path, SortedSet<file name>>
-    SortedMap<String,SortedSet<String>> candidates;
+  private static class Data {
+    private final int version;
+
+    /*
+     * The root tablet will only have a single dir on each volume. Therefore, 
root file paths will
+     * have a small set of unique prefixes. The following map is structured to 
avoid storing the
+     * same dir prefix over and over in JSon and java.
+     *
+     * SortedMap<dir path, SortedSet<file name>>
+     */
+    private final SortedMap<String,SortedSet<String>> candidates;
+
+    public Data(int version, SortedMap<String,SortedSet<String>> candidates) {
+      this.version = version;
+      this.candidates = candidates;
+    }
   }
 
-  /*
-   * The root tablet will only have a single dir on each volume. Therefore 
root file paths will have
-   * a small set of unique prefixes. The following map is structured to avoid 
storing the same dir
-   * prefix over and over in JSon and java.
-   *
-   * SortedMap<dir path, SortedSet<file name>>
-   */
-  private SortedMap<String,SortedSet<String>> candidates;
-
   public RootGcCandidates() {
-    this.candidates = new TreeMap<>();
+    this.data = new Data(VERSION, new TreeMap<>());
   }
 
-  private RootGcCandidates(SortedMap<String,SortedSet<String>> candidates) {
-    this.candidates = candidates;
+  public RootGcCandidates(String jsonString) {
+    this.data = gson.fromJson(jsonString, Data.class);
+    checkArgument(data.version == VERSION, "Invalid Root Table GC Candidates 
JSON version %s",
+        data.version);
+    data.candidates.forEach((parent, files) -> {
+      checkArgument(!parent.isBlank(), "Blank parent dir in %s", 
data.candidates);
+      checkArgument(!files.isEmpty(), "Empty files for dir %s", parent);
+    });
   }
 
-  public void add(Iterator<StoredTabletFile> refs) {
-    refs.forEachRemaining(ref -> {
-      String parent = ref.getPath().getParent().toString();
-      candidates.computeIfAbsent(parent, k -> new 
TreeSet<>()).add(ref.getFileName());
-    });
+  public void add(Stream<StoredTabletFile> refs) {
+    refs.forEach(ref -> data.candidates
+        .computeIfAbsent(ref.getPath().getParent().toString(), k -> new 
TreeSet<>())
+        .add(ref.getFileName()));
   }
 
-  public void remove(Collection<String> refs) {
-    refs.forEach(ref -> {
-      Path path = new Path(ref);
-      String parent = path.getParent().toString();
-      String name = path.getName();
-
-      SortedSet<String> names = candidates.get(parent);
-      if (names != null) {
-        names.remove(name);
-        if (names.isEmpty()) {
-          candidates.remove(parent);
-        }
-      }
-    });
+  public void remove(Stream<String> refs) {
+    refs.map(Path::new).forEach(
+        path -> data.candidates.computeIfPresent(path.getParent().toString(), 
(key, values) -> {
+          values.remove(path.getName());
+          return values.isEmpty() ? null : values;
+        }));
   }
 
-  public Stream<String> stream() {
-    return candidates.entrySet().stream().flatMap(entry -> {
+  public Stream<String> sortedStream() {
+    return data.candidates.entrySet().stream().flatMap(entry -> {
       String parent = entry.getKey();
       SortedSet<String> names = entry.getValue();
-      return names.stream().map(name -> new Path(parent, name).toString());
-    });
+      return names.stream().map(name -> new Path(parent, name));
+    }).map(Path::toString).sorted();
   }
 
   public String toJson() {
-    GSonData gd = new GSonData();
-    gd.candidates = candidates;
-    return GSON.toJson(gd);
-  }
-
-  public static RootGcCandidates fromJson(String json) {
-    GSonData gd = GSON.fromJson(json, GSonData.class);
-
-    Preconditions.checkArgument(gd.version == 1);
-
-    return new RootGcCandidates(gd.candidates);
+    return gson.toJson(data);
   }
 
-  public static RootGcCandidates fromJson(byte[] json) {
-    return fromJson(new String(json, UTF_8));
-  }
 }
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/RootTabletMutatorImpl.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/RootTabletMutatorImpl.java
index 7681b5988d..dc1d157d86 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/RootTabletMutatorImpl.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/RootTabletMutatorImpl.java
@@ -103,7 +103,7 @@ public class RootTabletMutatorImpl extends 
TabletMutatorBase implements Ample.Ta
       // TODO examine implementation of getZooReaderWriter().mutate()
       context.getZooReaderWriter().mutateOrCreate(zpath, new byte[0], currVal 
-> {
         String currJson = new String(currVal, UTF_8);
-        var rtm = RootTabletMetadata.fromJson(currJson);
+        var rtm = new RootTabletMetadata(currJson);
         rtm.update(mutation);
         String newJson = rtm.toJson();
         log.debug("mutation: from:[{}] to: [{}]", currJson, newJson);
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/ServerAmpleImpl.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/ServerAmpleImpl.java
index 6da3a28827..08e20434fb 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/ServerAmpleImpl.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/ServerAmpleImpl.java
@@ -84,7 +84,7 @@ public class ServerAmpleImpl extends AmpleImpl implements 
Ample {
     try {
       context.getZooReaderWriter().mutateOrCreate(zpath, new byte[0], currVal 
-> {
         String currJson = new String(currVal, UTF_8);
-        RootGcCandidates rgcc = RootGcCandidates.fromJson(currJson);
+        RootGcCandidates rgcc = new RootGcCandidates(currJson);
         log.debug("Root GC candidates before change : {}", currJson);
         mutator.accept(rgcc);
         String newJson = rgcc.toJson();
@@ -106,7 +106,7 @@ public class ServerAmpleImpl extends AmpleImpl implements 
Ample {
   public void putGcCandidates(TableId tableId, Collection<StoredTabletFile> 
candidates) {
 
     if (RootTable.ID.equals(tableId)) {
-      mutateRootGcCandidates(rgcc -> rgcc.add(candidates.iterator()));
+      mutateRootGcCandidates(rgcc -> rgcc.add(candidates.stream()));
       return;
     }
 
@@ -125,8 +125,7 @@ public class ServerAmpleImpl extends AmpleImpl implements 
Ample {
     if (RootTable.ID.equals(tableId)) {
 
       // Directories are unexpected for the root tablet, so convert to stored 
tablet file
-      mutateRootGcCandidates(
-          rgcc -> 
rgcc.add(candidates.stream().map(StoredTabletFile::new).iterator()));
+      mutateRootGcCandidates(rgcc -> 
rgcc.add(candidates.stream().map(StoredTabletFile::new)));
       return;
     }
 
@@ -143,7 +142,7 @@ public class ServerAmpleImpl extends AmpleImpl implements 
Ample {
   public void deleteGcCandidates(DataLevel level, Collection<String> paths) {
 
     if (level == DataLevel.ROOT) {
-      mutateRootGcCandidates(rgcc -> rgcc.remove(paths));
+      mutateRootGcCandidates(rgcc -> rgcc.remove(paths.stream()));
       return;
     }
 
@@ -162,14 +161,14 @@ public class ServerAmpleImpl extends AmpleImpl implements 
Ample {
   public Iterator<String> getGcCandidates(DataLevel level) {
     if (level == DataLevel.ROOT) {
       var zooReader = context.getZooReader();
-      byte[] json;
+      byte[] jsonBytes;
       try {
-        json = zooReader.getData(context.getZooKeeperRoot() + 
RootTable.ZROOT_TABLET_GC_CANDIDATES);
+        jsonBytes =
+            zooReader.getData(context.getZooKeeperRoot() + 
RootTable.ZROOT_TABLET_GC_CANDIDATES);
       } catch (KeeperException | InterruptedException e) {
         throw new RuntimeException(e);
       }
-      Stream<String> candidates = 
RootGcCandidates.fromJson(json).stream().sorted();
-      return candidates.iterator();
+      return new RootGcCandidates(new String(jsonBytes, 
UTF_8)).sortedStream().iterator();
     } else if (level == DataLevel.METADATA || level == DataLevel.USER) {
       Range range = DeletesSection.getRange();
 
diff --git 
a/server/base/src/test/java/org/apache/accumulo/server/manager/state/RootTabletStateStoreTest.java
 
b/server/base/src/test/java/org/apache/accumulo/server/manager/state/RootTabletStateStoreTest.java
index f6991550de..4564466bb1 100644
--- 
a/server/base/src/test/java/org/apache/accumulo/server/manager/state/RootTabletStateStoreTest.java
+++ 
b/server/base/src/test/java/org/apache/accumulo/server/manager/state/RootTabletStateStoreTest.java
@@ -19,6 +19,7 @@
 package org.apache.accumulo.server.manager.state;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.accumulo.server.init.ZooKeeperInitializer.getInitialRootTabletJson;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -49,14 +50,14 @@ public class RootTabletStateStoreTest {
 
   private static class TestAmple implements Ample {
 
-    private String json = new String(
-        RootTabletMetadata.getInitialJson("dir", 
"hdfs://nn/acc/tables/some/dir/0000.rf"), UTF_8);
+    private String json =
+        new String(getInitialRootTabletJson("dir", 
"hdfs://nn/acc/tables/some/dir/0000.rf"), UTF_8);
 
     @Override
     public TabletMetadata readTablet(KeyExtent extent, ReadConsistency rc,
         ColumnType... colsToFetch) {
       Preconditions.checkArgument(extent.equals(RootTable.EXTENT));
-      return RootTabletMetadata.fromJson(json).convertToTabletMetadata();
+      return new RootTabletMetadata(json).toTabletMetadata();
     }
 
     @Override
@@ -73,10 +74,8 @@ public class RootTabletStateStoreTest {
         public void mutate() {
           Mutation m = getMutation();
 
-          RootTabletMetadata rtm = RootTabletMetadata.fromJson(json);
-
+          var rtm = new RootTabletMetadata(json);
           rtm.update(m);
-
           json = rtm.toJson();
         }
       };
diff --git 
a/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java 
b/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
index 36d1f04890..fcee1bf48e 100644
--- a/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
@@ -78,8 +78,7 @@ public class CountNameNodeOpsBulkIT extends 
ConfigurableMacBase {
     URL url = new URL(uri + "/jmx");
     log.debug("Fetching web page " + url);
     String jsonString = FunctionalTestUtils.readWebPage(url).body();
-    Gson gson = new Gson();
-    Map<?,?> jsonObject = gson.fromJson(jsonString, Map.class);
+    Map<?,?> jsonObject = new Gson().fromJson(jsonString, Map.class);
     List<?> beans = (List<?>) jsonObject.get("beans");
     for (Object bean : beans) {
       Map<?,?> map = (Map<?,?>) bean;

Reply via email to