This is an automated email from the ASF dual-hosted git repository.
domgarguilo pushed a commit to branch elasticity
in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/elasticity by this push:
new 1ebe3352a2 Ensure serialized version of SelectedFiles is comparable
(#3575)
1ebe3352a2 is described below
commit 1ebe3352a2db8ef856fbaf5b8257cd6424976893
Author: Dom G <[email protected]>
AuthorDate: Thu Jul 13 14:47:48 2023 -0400
Ensure serialized version of SelectedFiles is comparable (#3575)
* use custom TypeAdapter for serialization of json
* add tests
---------
Co-authored-by: Keith Turner <[email protected]>
---
core/pom.xml | 5 +
.../core/metadata/schema/SelectedFiles.java | 130 +++++++++----
.../core/metadata/schema/SelectedFilesTest.java | 207 +++++++++++++++++++++
3 files changed, 307 insertions(+), 35 deletions(-)
diff --git a/core/pom.xml b/core/pom.xml
index ebc84c893e..c486923432 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -168,6 +168,11 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<testResources>
diff --git
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/SelectedFiles.java
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/SelectedFiles.java
index e0863f53ba..73f005199b 100644
---
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/SelectedFiles.java
+++
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/SelectedFiles.java
@@ -18,10 +18,11 @@
*/
package org.apache.accumulo.core.metadata.schema;
-import static java.util.stream.Collectors.toList;
-import static org.apache.accumulo.core.util.LazySingletons.GSON;
-
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -29,6 +30,11 @@ import org.apache.accumulo.core.fate.FateTxId;
import org.apache.accumulo.core.metadata.StoredTabletFile;
import com.google.common.base.Preconditions;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
/**
* This class is used to manage the set of files selected for a user
compaction for a tablet.
@@ -36,50 +42,82 @@ import com.google.common.base.Preconditions;
public class SelectedFiles {
private final Set<StoredTabletFile> files;
-
private final boolean initiallySelectedAll;
private final long fateTxId;
- private final String metadataValue;
-
- // This class is used to serialize and deserialize this class using GSon.
Any changes to this
- // class must consider persisted data.
- private static class GSonData {
- String txid;
- boolean selAll;
- List<String> files;
- }
+
+ private String metadataValue;
+
+ private static final Gson GSON = new GsonBuilder()
+ .registerTypeAdapter(SelectedFiles.class, new
SelectedFilesTypeAdapter()).create();
public SelectedFiles(Set<StoredTabletFile> files, boolean
initiallySelectedAll, long fateTxId) {
Preconditions.checkArgument(files != null && !files.isEmpty());
this.files = Set.copyOf(files);
this.initiallySelectedAll = initiallySelectedAll;
this.fateTxId = fateTxId;
-
- GSonData jData = new GSonData();
- // sort to make the serialized version equals when the sets are equal
- jData.files =
-
files.stream().map(StoredTabletFile::getMetaUpdateDelete).sorted().collect(toList());
- jData.txid = FateTxId.formatTid(fateTxId);
- jData.selAll = initiallySelectedAll;
- // ELASITICITY_TODO need the produced json to always be the same when the
input data is the same
- // as its used for comparison. Need unit test to ensure this behavior.
- metadataValue = GSON.get().toJson(jData);
}
- private SelectedFiles(Set<StoredTabletFile> files, boolean
initiallySelectedAll, long fateTxId,
- String metaVal) {
- Preconditions.checkArgument(files != null && !files.isEmpty());
- this.files = files;
- this.initiallySelectedAll = initiallySelectedAll;
- this.fateTxId = fateTxId;
- this.metadataValue = metaVal;
+ private static class SelectedFilesTypeAdapter extends
TypeAdapter<SelectedFiles> {
+
+ @Override
+ public void write(JsonWriter out, SelectedFiles selectedFiles) throws
IOException {
+ out.beginObject();
+ out.name("txid").value(FateTxId.formatTid(selectedFiles.getFateTxId()));
+ out.name("selAll").value(selectedFiles.initiallySelectedAll());
+ out.name("files").beginArray();
+ // sort the data to make serialized json comparable
+
selectedFiles.getFiles().stream().map(StoredTabletFile::getMetaUpdateDelete).sorted()
+ .forEach(file -> {
+ try {
+ out.value(file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Failed to add file " + file + " to the JSON files array",
e);
+ }
+ });
+ out.endArray();
+ out.endObject();
+ }
+
+ @Override
+ public SelectedFiles read(JsonReader in) throws IOException {
+ long fateTxId = 0L;
+ boolean selAll = false;
+ List<String> files = new ArrayList<>();
+
+ in.beginObject();
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "txid":
+ fateTxId = FateTxId.fromString(in.nextString());
+ break;
+ case "selAll":
+ selAll = in.nextBoolean();
+ break;
+ case "files":
+ in.beginArray();
+ while (in.hasNext()) {
+ files.add(in.nextString());
+ }
+ in.endArray();
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown field name : " + name);
+ }
+ }
+ in.endObject();
+
+ Set<StoredTabletFile> tabletFiles =
+
files.stream().map(StoredTabletFile::new).collect(Collectors.toSet());
+
+ return new SelectedFiles(tabletFiles, selAll, fateTxId);
+ }
+
}
public static SelectedFiles from(String json) {
- GSonData jData = GSON.get().fromJson(json, GSonData.class);
- return new SelectedFiles(
-
jData.files.stream().map(StoredTabletFile::new).collect(Collectors.toSet()),
jData.selAll,
- FateTxId.fromString(jData.txid), json);
+ return GSON.fromJson(json, SelectedFiles.class);
}
public Set<StoredTabletFile> getFiles() {
@@ -95,7 +133,29 @@ public class SelectedFiles {
}
public String getMetadataValue() {
- return metadataValue;
+ if (this.metadataValue == null) {
+ // use the custom TypeAdapter to create the json
+ this.metadataValue = GSON.toJson(this);
+ }
+ return this.metadataValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SelectedFiles other = (SelectedFiles) obj;
+ return fateTxId == other.fateTxId && files.equals(other.files)
+ && initiallySelectedAll == other.initiallySelectedAll;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fateTxId, files, initiallySelectedAll);
}
}
diff --git
a/core/src/test/java/org/apache/accumulo/core/metadata/schema/SelectedFilesTest.java
b/core/src/test/java/org/apache/accumulo/core/metadata/schema/SelectedFilesTest.java
new file mode 100644
index 0000000000..555f3b2239
--- /dev/null
+++
b/core/src/test/java/org/apache/accumulo/core/metadata/schema/SelectedFilesTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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
+ *
+ * https://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.core.metadata.schema;
+
+import static org.apache.accumulo.core.util.LazySingletons.RANDOM;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.accumulo.core.fate.FateTxId;
+import org.apache.accumulo.core.metadata.StoredTabletFile;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class SelectedFilesTest {
+
+ final String filePathPrefix =
"hdfs://namenode:9020/accumulo/tables/1/default_tablet/";
+
+ /**
+ * Make sure a SelectedFiles object is equal to another SelectedFiles object
that was created
+ * using its metadata json
+ */
+ @Test
+ public void testSerializationDeserialization() {
+ Set<StoredTabletFile> files = getStoredTabletFiles(2);
+
+ SelectedFiles original = new SelectedFiles(files, true, 12345L);
+
+ String json = original.getMetadataValue();
+ SelectedFiles deserialized = SelectedFiles.from(json);
+
+ assertEquals(original, deserialized);
+ assertEquals(json, deserialized.getMetadataValue());
+ }
+
+ /**
+ * Ensure two SelectedFiles objects created with the same parameters are
equal
+ */
+ @Test
+ public void testEqualSerialization() {
+ Set<StoredTabletFile> files = getStoredTabletFiles(16);
+
+ SelectedFiles sf1 = new SelectedFiles(files, true, 12345L);
+ SelectedFiles sf2 = new SelectedFiles(files, true, 12345L);
+
+ assertEquals(sf1.getMetadataValue(), sf2.getMetadataValue());
+ assertEquals(sf1, sf2);
+ }
+
+ /**
+ * Ensure that SelectedFiles objects created with file arrays of differing
order yet equal entries
+ * are correctly serialized
+ */
+ @Test
+ public void testDifferentFilesOrdering() {
+ Set<StoredTabletFile> files = getStoredTabletFiles(16);
+ SortedSet<StoredTabletFile> sortedFiles = new TreeSet<>(files);
+
+ assertEquals(files, sortedFiles, "Entries in test file sets should be the
same");
+ assertNotEquals(files.toString(), sortedFiles.toString(),
+ "Order of files set should differ for this test case");
+
+ SelectedFiles sf1 = new SelectedFiles(files, false, 654123L);
+ SelectedFiles sf2 = new SelectedFiles(sortedFiles, false, 654123L);
+
+ assertEquals(sf1.getMetadataValue(), sf2.getMetadataValue());
+ assertEquals(sf1, sf2);
+ }
+
+ /**
+ * Ensure that two SelectedFiles objects, one created with a subset of files
present in the other,
+ * are not equal
+ */
+ @Test
+ public void testJsonSuperSetSubset() {
+ Set<StoredTabletFile> filesSuperSet = getStoredTabletFiles(3);
+ Set<StoredTabletFile> filesSubSet = new HashSet<>(filesSuperSet);
+ // Remove an element to create a subset
+ filesSubSet.remove(filesSubSet.iterator().next());
+
+ SelectedFiles superSetSelectedFiles = new SelectedFiles(filesSuperSet,
true, 123456L);
+ SelectedFiles subSetSelectedFiles = new SelectedFiles(filesSubSet, true,
123456L);
+
+ String superSetJson = superSetSelectedFiles.getMetadataValue();
+ String subSetJson = subSetSelectedFiles.getMetadataValue();
+
+ assertNotEquals(superSetJson, subSetJson, "The metadata jsons should have
differing file sets");
+
+ // Deserialize the JSON strings back to SelectedFiles and check for
equality with the original
+ // objects
+ SelectedFiles superSetDeserialized = SelectedFiles.from(superSetJson);
+ SelectedFiles subSetDeserialized = SelectedFiles.from(subSetJson);
+
+ assertEquals(superSetSelectedFiles, superSetDeserialized);
+ assertEquals(subSetSelectedFiles, subSetDeserialized);
+
+ assertNotEquals(superSetDeserialized, subSetDeserialized,
+ "The objects should have differing file sets");
+ }
+
+ private static Stream<Arguments> provideTestJsons() {
+ return Stream.of(Arguments.of("123456", true, 12), Arguments.of("123456",
false, 12),
+ Arguments.of("123456", false, 23), Arguments.of("654321", false, 23),
+ Arguments.of("AE56E", false, 23));
+ }
+
+ /**
+ * Ensure that SelectedFiles objects that are created using {@link
SelectedFiles#from(String)} are
+ * created correctly
+ */
+ @ParameterizedTest
+ @MethodSource("provideTestJsons")
+ public void testJsonStrings(String txid, boolean selAll, int numPaths) {
+ List<String> paths = getFilePaths(numPaths);
+
+ // should be resilient to unordered file arrays
+ Collections.shuffle(paths, RANDOM.get());
+
+ // construct a json from the given parameters
+ String json = getJson(txid, selAll, paths);
+
+ // create a SelectedFiles object using the json
+ SelectedFiles selectedFiles = SelectedFiles.from(json);
+
+ // ensure all parts of the SelectedFiles object are correct
+ assertEquals(Long.parseLong(txid, 16), selectedFiles.getFateTxId());
+ assertEquals(selAll, selectedFiles.initiallySelectedAll());
+ Set<StoredTabletFile> expectedStoredTabletFiles =
filePathsToStoredTabletFiles(paths);
+ assertEquals(expectedStoredTabletFiles, selectedFiles.getFiles());
+
+ Collections.sort(paths);
+ String jsonWithSortedFiles = getJson(txid, selAll, paths);
+ assertEquals(jsonWithSortedFiles, selectedFiles.getMetadataValue());
+ }
+
+ /**
+ * Creates a json suitable to create a SelectedFiles object from. The given
parameters will be
+ * inserted into the returned json String. Example of returned json String:
+ *
+ * <pre>
+ * {
+ * "txid": "FATE[123456]",
+ * "selAll": true,
+ * "files": ["/path/to/file1.rf", "/path/to/file2.rf"]
+ * }
+ * </pre>
+ */
+ private static String getJson(String txid, boolean selAll, List<String>
paths) {
+ String filesJsonArray =
+ paths.stream().map(path -> "'" + path +
"'").collect(Collectors.joining(","));
+ return ("{'txid':'" + FateTxId.formatTid(Long.parseLong(txid, 16)) +
"','selAll':" + selAll
+ + ",'files':[" + filesJsonArray + "]}").replace('\'', '\"');
+ }
+
+ /**
+ * Generate the given number of rfile path strings
+ */
+ private List<String> getFilePaths(int numOfFiles) {
+ List<String> paths = new ArrayList<>(numOfFiles);
+ for (int i = 0; i < numOfFiles; i++) {
+ paths.add(filePathPrefix + "F0000" + i + ".rf");
+ }
+ return paths;
+ }
+
+ /**
+ * Convert a list of file paths to StoredTabletFile objects
+ */
+ private Set<StoredTabletFile> filePathsToStoredTabletFiles(List<String>
filePaths) {
+ return
filePaths.stream().map(StoredTabletFile::new).collect(Collectors.toSet());
+ }
+
+ /**
+ * Generate the given number of StoredTabletFile objects
+ */
+ private Set<StoredTabletFile> getStoredTabletFiles(int numOfFiles) {
+ return filePathsToStoredTabletFiles(getFilePaths(numOfFiles));
+ }
+
+}