This is an automated email from the ASF dual-hosted git repository.
vinoth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-hudi.git
The following commit(s) were added to refs/heads/master by this push:
new a0e62b7 Bucketized Bloom Filter checking
a0e62b7 is described below
commit a0e62b79192874808193247053b502746a8bef1d
Author: Vinoth Chandar <[email protected]>
AuthorDate: Wed May 8 20:20:58 2019 -0700
Bucketized Bloom Filter checking
- Tackles the skew seen in sort based partitioning/checking
- Parameterized the HoodieBloomIndex test
- Config to turn on/off (on by default)
- Unit tests & also tested at scale
---
.gitignore | 7 +
.../com/uber/hoodie/config/HoodieIndexConfig.java | 23 ++
.../com/uber/hoodie/config/HoodieWriteConfig.java | 8 +
.../bloom/BucketizedBloomCheckPartitioner.java | 155 ++++++++++++
.../uber/hoodie/index/bloom/HoodieBloomIndex.java | 91 +++----
.../bloom/TestBucketizedBloomCheckPartitioner.java | 83 +++++++
.../hoodie/index/bloom/TestHoodieBloomIndex.java | 270 ++++++++++++---------
7 files changed, 472 insertions(+), 165 deletions(-)
diff --git a/.gitignore b/.gitignore
index fa0127e..3304d70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,10 @@ local.properties
# Maven
#######################################
dependency-reduced-pom.xml
+
+
+#######################################
+# Docker
+#######################################
+hoodie-integ-test/compose_env
+
diff --git
a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieIndexConfig.java
b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieIndexConfig.java
index 3e4772a..3e4a1bf 100644
--- a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieIndexConfig.java
+++ b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieIndexConfig.java
@@ -47,6 +47,15 @@ public class HoodieIndexConfig extends DefaultHoodieConfig {
public static final String DEFAULT_BLOOM_INDEX_USE_CACHING = "true";
public static final String BLOOM_INDEX_TREE_BASED_FILTER_PROP =
"hoodie.bloom.index.use.treebased.filter";
public static final String DEFAULT_BLOOM_INDEX_TREE_BASED_FILTER = "true";
+ // TODO: On by default. Once stable, we will remove the other mode.
+ public static final String BLOOM_INDEX_BUCKETIZED_CHECKING_PROP =
"hoodie.bloom.index.bucketized.checking";
+ public static final String DEFAULT_BLOOM_INDEX_BUCKETIZED_CHECKING = "true";
+ // 1B bloom filter checks happen in 250 seconds. 500ms to read a bloom
filter.
+ // 10M checks in 2500ms, thus amortizing the cost of reading bloom filter
across partitions.
+ public static final String BLOOM_INDEX_KEYS_PER_BUCKET_PROP =
"hoodie.bloom.index.keys.per.bucket";
+ public static final String DEFAULT_BLOOM_INDEX_KEYS_PER_BUCKET = "10000000";
+
+
public static final String BLOOM_INDEX_INPUT_STORAGE_LEVEL =
"hoodie.bloom.index.input.storage" + ".level";
public static final String DEFAULT_BLOOM_INDEX_INPUT_STORAGE_LEVEL =
"MEMORY_AND_DISK_SER";
@@ -118,6 +127,16 @@ public class HoodieIndexConfig extends DefaultHoodieConfig
{
return this;
}
+ public Builder bloomIndexBucketizedChecking(boolean bucketizedChecking) {
+ props.setProperty(BLOOM_INDEX_BUCKETIZED_CHECKING_PROP,
String.valueOf(bucketizedChecking));
+ return this;
+ }
+
+ public Builder bloomIndexKeysPerBucket(int keysPerBucket) {
+ props.setProperty(BLOOM_INDEX_KEYS_PER_BUCKET_PROP,
String.valueOf(keysPerBucket));
+ return this;
+ }
+
public Builder withBloomIndexInputStorageLevel(String level) {
props.setProperty(BLOOM_INDEX_INPUT_STORAGE_LEVEL, level);
return this;
@@ -141,6 +160,10 @@ public class HoodieIndexConfig extends DefaultHoodieConfig
{
BLOOM_INDEX_INPUT_STORAGE_LEVEL,
DEFAULT_BLOOM_INDEX_INPUT_STORAGE_LEVEL);
setDefaultOnCondition(props,
!props.containsKey(BLOOM_INDEX_TREE_BASED_FILTER_PROP),
BLOOM_INDEX_TREE_BASED_FILTER_PROP,
DEFAULT_BLOOM_INDEX_TREE_BASED_FILTER);
+ setDefaultOnCondition(props,
!props.containsKey(BLOOM_INDEX_BUCKETIZED_CHECKING_PROP),
+ BLOOM_INDEX_BUCKETIZED_CHECKING_PROP,
DEFAULT_BLOOM_INDEX_BUCKETIZED_CHECKING);
+ setDefaultOnCondition(props,
!props.containsKey(BLOOM_INDEX_KEYS_PER_BUCKET_PROP),
+ BLOOM_INDEX_KEYS_PER_BUCKET_PROP,
DEFAULT_BLOOM_INDEX_KEYS_PER_BUCKET);
// Throws IllegalArgumentException if the value set is not a known
Hoodie Index Type
HoodieIndex.IndexType.valueOf(props.getProperty(INDEX_TYPE_PROP));
return config;
diff --git
a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java
b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java
index ab6570f..0834e1f 100644
--- a/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java
+++ b/hoodie-client/src/main/java/com/uber/hoodie/config/HoodieWriteConfig.java
@@ -328,6 +328,14 @@ public class HoodieWriteConfig extends DefaultHoodieConfig
{
return
Boolean.parseBoolean(props.getProperty(HoodieIndexConfig.BLOOM_INDEX_TREE_BASED_FILTER_PROP));
}
+ public boolean useBloomIndexBucketizedChecking() {
+ return
Boolean.parseBoolean(props.getProperty(HoodieIndexConfig.BLOOM_INDEX_BUCKETIZED_CHECKING_PROP));
+ }
+
+ public int getBloomIndexKeysPerBucket() {
+ return
Integer.parseInt(props.getProperty(HoodieIndexConfig.BLOOM_INDEX_KEYS_PER_BUCKET_PROP));
+ }
+
public StorageLevel getBloomIndexInputStorageLevel() {
return StorageLevel
.fromString(props.getProperty(HoodieIndexConfig.BLOOM_INDEX_INPUT_STORAGE_LEVEL));
diff --git
a/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/BucketizedBloomCheckPartitioner.java
b/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/BucketizedBloomCheckPartitioner.java
new file mode 100644
index 0000000..5bc47e2
--- /dev/null
+++
b/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/BucketizedBloomCheckPartitioner.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2019 Uber Technologies, Inc. ([email protected])
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.uber.hoodie.index.bloom;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.hash.Hashing;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.spark.Partitioner;
+
+/**
+ * Partitions bloom filter checks by spreading out comparisons across buckets
of work.
+ *
+ * Each bucket incurs the following cost
+ * <pre>
+ * 1) Read bloom filter from file footer
+ * 2) Check keys against bloom filter
+ * 3) [Conditional] If any key had a hit, open file and check
+ * </pre>
+ *
+ * The partitioner performs a two phase bin packing algorithm, to pack enough
work into each bucket such that cost of
+ * (1) & (3) is amortized. Also, avoids any skews in the sort based approach,
by directly partitioning by the file to be
+ * checked against and ensuring each partition has similar number of buckets.
Performance tests show that this approach
+ * could bound the amount of skew to std_dev(numberOfBucketsPerPartition) *
cost of (3), lower than sort partitioning.
+ *
+ * Approach has two goals :
+ * <pre>
+ * 1) Pack as many buckets from same file group into same partition, to
amortize cost of (1) and (2) further
+ * 2) Spread buckets across partitions evenly to achieve skew reduction
+ * </pre>
+ */
+public class BucketizedBloomCheckPartitioner extends Partitioner {
+
+ private static Logger logger =
LogManager.getLogger(BucketizedBloomCheckPartitioner.class);
+
+ private int partitions;
+
+ /**
+ * Stores the final mapping of a file group to a list of partitions for its
keys.
+ */
+ private Map<String, List<Integer>> fileGroupToPartitions;
+
+ /**
+ * Create a partitioner that computes a plan based on provided workload
characteristics.
+ *
+ * @param targetPartitions maximum number of partitions to target
+ * @param fileGroupToComparisons number of expected comparisons per file
group
+ * @param keysPerBucket maximum number of keys to pack in a single bucket
+ */
+ public BucketizedBloomCheckPartitioner(int targetPartitions, Map<String,
Long> fileGroupToComparisons,
+ int keysPerBucket) {
+ this.fileGroupToPartitions = new HashMap<>();
+
+ Map<String, Integer> bucketsPerFileGroup = new HashMap<>();
+ // Compute the buckets needed per file group, using simple uniform
distribution
+ fileGroupToComparisons.forEach((f, c) ->
+ bucketsPerFileGroup.put(f, (int) Math.ceil((c * 1.0) /
keysPerBucket)));
+ int totalBuckets = bucketsPerFileGroup.values().stream().mapToInt(i ->
i).sum();
+ // If totalBuckets > targetPartitions, no need to have extra partitions
+ this.partitions = Math.min(targetPartitions, totalBuckets);
+
+ // PHASE 1 : start filling upto minimum number of buckets into partitions,
taking all but one bucket from each file
+ // This tries to first optimize for goal 1 above, with knowledge that each
partition needs a certain minimum number
+ // of buckets and assigns buckets in the same order as file groups. If we
were to simply round robin, then buckets
+ // for a file group is more or less guaranteed to be placed on different
partitions all the time.
+ int minBucketsPerPartition = Math.max((int) Math.floor((1.0 *
totalBuckets) / partitions), 1);
+ logger.info(String.format("TotalBuckets %d, min_buckets/partition %d",
totalBuckets, minBucketsPerPartition));
+ int[] bucketsFilled = new int[partitions];
+ Map<String, AtomicInteger> bucketsFilledPerFileGroup = new HashMap<>();
+ int partitionIndex = 0;
+ for (Map.Entry<String, Integer> e : bucketsPerFileGroup.entrySet()) {
+ for (int b = 0; b < Math.max(1, e.getValue() - 1); b++) {
+ // keep filled counts upto date
+ bucketsFilled[partitionIndex]++;
+ AtomicInteger cnt = bucketsFilledPerFileGroup.getOrDefault(e.getKey(),
new AtomicInteger(0));
+ cnt.incrementAndGet();
+ bucketsFilledPerFileGroup.put(e.getKey(), cnt);
+
+ // mark this partition against the file group
+ List<Integer> partitionList =
this.fileGroupToPartitions.getOrDefault(e.getKey(), new ArrayList<>());
+ partitionList.add(partitionIndex);
+ this.fileGroupToPartitions.put(e.getKey(), partitionList);
+
+ // switch to new partition if needed
+ if (bucketsFilled[partitionIndex] >= minBucketsPerPartition) {
+ partitionIndex = (partitionIndex + 1) % partitions;
+ }
+ }
+ }
+
+ // PHASE 2 : for remaining unassigned buckets, round robin over partitions
once. Since we withheld 1 bucket from
+ // each file group uniformly, this remaining is also an uniform mix across
file groups. We just round robin to
+ // optimize for goal 2.
+ for (Map.Entry<String, Integer> e : bucketsPerFileGroup.entrySet()) {
+ int remaining = e.getValue() -
bucketsFilledPerFileGroup.get(e.getKey()).intValue();
+ for (int r = 0; r < remaining; r++) {
+ // mark this partition against the file group
+ this.fileGroupToPartitions.get(e.getKey()).add(partitionIndex);
+ bucketsFilled[partitionIndex]++;
+ partitionIndex = (partitionIndex + 1) % partitions;
+ }
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Partitions assigned per file groups :" +
fileGroupToPartitions);
+ StringBuilder str = new StringBuilder();
+ for (int i = 0; i < bucketsFilled.length; i++) {
+ str.append("p" + i + " : " + bucketsFilled[i] + ",");
+ }
+ logger.debug("Num buckets assigned per file group :" + str);
+ }
+ }
+
+ @Override
+ public int numPartitions() {
+ return partitions;
+ }
+
+ @Override
+ public int getPartition(Object key) {
+ String[] parts = ((String) key).split("#");
+ String fileName = parts[0];
+ final long hashOfKey = Hashing.md5().hashString(parts[1],
StandardCharsets.UTF_8).asLong();
+ List<Integer> candidatePartitions = fileGroupToPartitions.get(fileName);
+ int idx = (int) Math.floorMod(hashOfKey, candidatePartitions.size());
+ assert idx >= 0;
+ return candidatePartitions.get(idx);
+ }
+
+ @VisibleForTesting
+ Map<String, List<Integer>> getFileGroupToPartitions() {
+ return fileGroupToPartitions;
+ }
+}
diff --git
a/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/HoodieBloomIndex.java
b/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/HoodieBloomIndex.java
index 9ac8db0..ccafc56 100644
---
a/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/HoodieBloomIndex.java
+++
b/hoodie-client/src/main/java/com/uber/hoodie/index/bloom/HoodieBloomIndex.java
@@ -39,9 +39,9 @@ import com.uber.hoodie.exception.MetadataNotFoundException;
import com.uber.hoodie.index.HoodieIndex;
import com.uber.hoodie.table.HoodieTable;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
@@ -151,53 +151,53 @@ public class HoodieBloomIndex<T extends
HoodieRecordPayload> extends HoodieIndex
// Step 3: Obtain a RDD, for each incoming record, that already exists,
with the file id,
// that contains it.
- int parallelism = autoComputeParallelism(recordsPerPartition,
partitionToFileInfo,
+ Map<String, Long> comparisonsPerFileGroup =
computeComparisonsPerFileGroup(recordsPerPartition, partitionToFileInfo,
partitionRecordKeyPairRDD);
- return findMatchingFilesForRecordKeys(partitionToFileInfo,
- partitionRecordKeyPairRDD, parallelism, hoodieTable.getMetaClient());
+ int safeParallelism = computeSafeParallelism(recordsPerPartition,
comparisonsPerFileGroup);
+ int joinParallelism =
determineParallelism(partitionRecordKeyPairRDD.partitions().size(),
safeParallelism);
+ return findMatchingFilesForRecordKeys(partitionToFileInfo,
partitionRecordKeyPairRDD, joinParallelism,
+ hoodieTable.getMetaClient(), comparisonsPerFileGroup);
}
/**
- * The index lookup can be skewed in three dimensions : #files, #partitions,
#records <p> To be able to smoothly
- * handle skews, we need to compute how to split each partitions into
subpartitions. We do it here, in a way that
- * keeps the amount of each Spark join partition to < 2GB. <p> If
- * {@link
com.uber.hoodie.config.HoodieIndexConfig#BLOOM_INDEX_PARALLELISM_PROP}
- * is specified as a NON-zero number, then that is used explicitly.
+ * Compute the estimated number of bloom filter comparisons to be performed
on each file group
*/
- private int autoComputeParallelism(final Map<String, Long>
recordsPerPartition,
+ private Map<String, Long> computeComparisonsPerFileGroup(final Map<String,
Long> recordsPerPartition,
final Map<String, List<BloomIndexFileInfo>> partitionToFileInfo,
JavaPairRDD<String, String> partitionRecordKeyPairRDD) {
- long totalComparisons = 0;
+ Map<String, Long> fileToComparisons;
if (config.getBloomIndexPruneByRanges()) {
// we will just try exploding the input and then count to determine
comparisons
- totalComparisons =
explodeRecordRDDWithFileComparisons(partitionToFileInfo,
- partitionRecordKeyPairRDD).count();
+ // FIX(vc): Only do sampling here and extrapolate?
+ fileToComparisons =
explodeRecordRDDWithFileComparisons(partitionToFileInfo,
+ partitionRecordKeyPairRDD).mapToPair(t -> t._2()).countByKey();
} else {
- // if not pruning by ranges, then each file in a partition needs to
compared against all
- // records for a partition.
- Map<String, Long> filesPerPartition =
partitionToFileInfo.entrySet().stream()
- .collect(Collectors.toMap(Map.Entry::getKey, e ->
Long.valueOf(e.getValue().size())));
- long totalFiles = 0;
- long totalRecords = 0;
- for (String partitionPath : recordsPerPartition.keySet()) {
- long numRecords = recordsPerPartition.get(partitionPath);
- long numFiles =
- filesPerPartition.getOrDefault(partitionPath, 1L);
-
- totalComparisons += numFiles * numRecords;
- totalFiles +=
- filesPerPartition.getOrDefault(partitionPath, 0L);
- totalRecords += numRecords;
- }
- logger.info("TotalRecords: " + totalRecords + ", TotalFiles: " +
totalFiles
- + ", TotalAffectedPartitions:" + recordsPerPartition.size());
+ fileToComparisons = new HashMap<>();
+ partitionToFileInfo.entrySet().stream().forEach(e -> {
+ for (BloomIndexFileInfo fileInfo : e.getValue()) {
+ //each file needs to be compared against all the records coming into
the partition
+ fileToComparisons.put(fileInfo.getFileName(),
recordsPerPartition.get(e.getKey()));
+ }
+ });
}
+ return fileToComparisons;
+ }
- // each partition will have an item per comparison.
+ /**
+ * Compute the minimum parallelism needed to play well with the spark 2GB
limitation.. The index lookup can be skewed
+ * in three dimensions : #files, #partitions, #records <p> To be able to
smoothly handle skews, we need to compute how
+ * to split each partitions into subpartitions. We do it here, in a way that
keeps the amount of each Spark join
+ * partition to < 2GB. <p> If {@link
com.uber.hoodie.config.HoodieIndexConfig#BLOOM_INDEX_PARALLELISM_PROP} is
+ * specified as a NON-zero number, then that is used explicitly.
+ */
+ int computeSafeParallelism(Map<String, Long> recordsPerPartition,
Map<String, Long> comparisonsPerFileGroup) {
+ long totalComparisons =
comparisonsPerFileGroup.values().stream().mapToLong(Long::longValue).sum();
+ long totalFiles = comparisonsPerFileGroup.size();
+ long totalRecords =
recordsPerPartition.values().stream().mapToLong(Long::longValue).sum();
int parallelism = (int) (totalComparisons /
MAX_ITEMS_PER_SHUFFLE_PARTITION + 1);
- logger.info(
- "Auto computed parallelism :" + parallelism + ", totalComparisons: " +
totalComparisons);
+ logger.info(String.format("TotalRecords %d, TotalFiles %d,
TotalAffectedPartitions %d, TotalComparisons %d, "
+ + "SafeParallelism %d", totalRecords, totalFiles,
recordsPerPartition.size(), totalComparisons, parallelism));
return parallelism;
}
@@ -329,18 +329,19 @@ public class HoodieBloomIndex<T extends
HoodieRecordPayload> extends HoodieIndex
@VisibleForTesting
JavaPairRDD<String, String> findMatchingFilesForRecordKeys(
final Map<String, List<BloomIndexFileInfo>> partitionToFileIndexInfo,
- JavaPairRDD<String, String> partitionRecordKeyPairRDD, int
totalSubpartitions, HoodieTableMetaClient metaClient) {
-
- int joinParallelism =
determineParallelism(partitionRecordKeyPairRDD.partitions().size(),
- totalSubpartitions);
-
+ JavaPairRDD<String, String> partitionRecordKeyPairRDD, int
shuffleParallelism, HoodieTableMetaClient metaClient,
+ Map<String, Long> fileGroupToComparisons) {
JavaPairRDD<String, Tuple2<String, HoodieKey>> fileSortedTripletRDD =
- explodeRecordRDDWithFileComparisons(
- partitionToFileIndexInfo, partitionRecordKeyPairRDD)
- // sort further based on filename, such that all checking for the
file can happen within
- // a single partition, on-the-fly
- .sortByKey(true, joinParallelism);
-
+ explodeRecordRDDWithFileComparisons(partitionToFileIndexInfo,
partitionRecordKeyPairRDD);
+ if (config.useBloomIndexBucketizedChecking()) {
+ BucketizedBloomCheckPartitioner partitioner = new
BucketizedBloomCheckPartitioner(shuffleParallelism,
+ fileGroupToComparisons, config.getBloomIndexKeysPerBucket());
+ fileSortedTripletRDD =
fileSortedTripletRDD.repartitionAndSortWithinPartitions(partitioner);
+ } else {
+ // sort further based on filename, such that all checking for the file
can happen within
+ // a single partition, on-the-fly
+ fileSortedTripletRDD = fileSortedTripletRDD.sortByKey(true,
shuffleParallelism);
+ }
return fileSortedTripletRDD.mapPartitionsWithIndex(
new HoodieBloomIndexCheckFunction(metaClient, config.getBasePath()),
true)
.flatMap(List::iterator)
diff --git
a/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestBucketizedBloomCheckPartitioner.java
b/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestBucketizedBloomCheckPartitioner.java
new file mode 100644
index 0000000..072e606
--- /dev/null
+++
b/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestBucketizedBloomCheckPartitioner.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2019 Uber Technologies, Inc. ([email protected])
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ */
+
+package com.uber.hoodie.index.bloom;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import com.uber.hoodie.common.util.collection.Pair;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Test;
+
+public class TestBucketizedBloomCheckPartitioner {
+
+ @Test
+ public void testAssignmentCorrectness() {
+ Map<String, Long> fileToComparisons = new HashMap<String, Long>() {
+ {
+ put("f1", 40L);
+ put("f2", 35L);
+ put("f3", 20L);
+ }
+ };
+ BucketizedBloomCheckPartitioner p = new BucketizedBloomCheckPartitioner(4,
fileToComparisons, 10);
+ Map<String, List<Integer>> assignments = p.getFileGroupToPartitions();
+ assertEquals("f1 should have 4 buckets", 4, assignments.get("f1").size());
+ assertEquals("f2 should have 4 buckets", 4, assignments.get("f2").size());
+ assertEquals("f3 should have 2 buckets", 2, assignments.get("f3").size());
+ assertArrayEquals("f1 spread across 3 partitions", new Integer[]{0, 0, 1,
3},
+ assignments.get("f1").toArray());
+ assertArrayEquals("f2 spread across 3 partitions", new Integer[]{1, 2, 2,
0},
+ assignments.get("f2").toArray());
+ assertArrayEquals("f3 spread across 2 partitions", new Integer[]{3, 1},
+ assignments.get("f3").toArray());
+ }
+
+ @Test
+ public void testUniformPacking() {
+ // evenly distribute 10 buckets/file across 100 partitions
+ Map<String, Long> comparisons1 = new HashMap<String, Long>() {
+ {
+ IntStream.range(0, 10).forEach(f -> put("f" + f, 100L));
+ }
+ };
+ BucketizedBloomCheckPartitioner partitioner = new
BucketizedBloomCheckPartitioner(100, comparisons1, 10);
+ Map<String, List<Integer>> assignments =
partitioner.getFileGroupToPartitions();
+ assignments.entrySet().stream().forEach(e -> assertEquals(10,
e.getValue().size()));
+ Map<Integer, Long> partitionToNumBuckets = assignments.entrySet().stream()
+ .flatMap(e -> e.getValue().stream().map(p -> Pair.of(p, e.getKey())))
+ .collect(Collectors.groupingBy(Pair::getLeft, Collectors.counting()));
+ partitionToNumBuckets.entrySet().stream().forEach(e -> assertEquals(1L,
e.getValue().longValue()));
+ }
+
+ @Test
+ public void testNumPartitions() {
+ Map<String, Long> comparisons1 = new HashMap<String, Long>() {
+ {
+ IntStream.range(0, 10).forEach(f -> put("f" + f, 100L));
+ }
+ };
+ BucketizedBloomCheckPartitioner p = new
BucketizedBloomCheckPartitioner(10000, comparisons1, 10);
+ assertEquals("num partitions must equal total buckets", 100,
p.numPartitions());
+ }
+}
diff --git
a/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestHoodieBloomIndex.java
b/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestHoodieBloomIndex.java
index a52881d..664c146 100644
---
a/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestHoodieBloomIndex.java
+++
b/hoodie-client/src/test/java/com/uber/hoodie/index/bloom/TestHoodieBloomIndex.java
@@ -36,11 +36,18 @@ import com.uber.hoodie.common.model.HoodieTestUtils;
import com.uber.hoodie.common.table.HoodieTableMetaClient;
import com.uber.hoodie.common.util.FSUtils;
import com.uber.hoodie.common.util.HoodieAvroUtils;
+import com.uber.hoodie.config.HoodieIndexConfig;
import com.uber.hoodie.config.HoodieWriteConfig;
import com.uber.hoodie.table.HoodieTable;
import java.io.File;
import java.io.IOException;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.avro.Schema;
import org.apache.commons.io.IOUtils;
@@ -53,8 +60,11 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import scala.Tuple2;
+@RunWith(Parameterized.class)
public class TestHoodieBloomIndex {
private JavaSparkContext jsc = null;
@@ -63,7 +73,21 @@ public class TestHoodieBloomIndex {
private String schemaStr;
private Schema schema;
- public TestHoodieBloomIndex() throws Exception {
+ private boolean rangePruning;
+ private boolean treeFiltering;
+ private boolean bucketizedChecking;
+
+ @Parameterized.Parameters(name = "{index}: Test with rangePruning={0},
treeFiltering ={1}, bucketizedChecking is:{2}")
+ public static Collection<Object[]> data() {
+ Object[][] data = new Object[][]{{true, true, true}, {false, true, true},
{true, true, false},
+ {true, false, true}};
+ return Arrays.asList(data);
+ }
+
+ public TestHoodieBloomIndex(boolean rangePruning, boolean treeFiltering,
boolean bucketizedChecking) {
+ this.rangePruning = rangePruning;
+ this.treeFiltering = treeFiltering;
+ this.bucketizedChecking = bucketizedChecking;
}
@Before
@@ -91,6 +115,18 @@ public class TestHoodieBloomIndex {
}
}
+ private HoodieWriteConfig makeConfig() {
+ HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath)
+ .withIndexConfig(HoodieIndexConfig.newBuilder()
+ .bloomIndexPruneByRanges(rangePruning)
+ .bloomIndexTreebasedFilter(treeFiltering)
+ .bloomIndexBucketizedChecking(bucketizedChecking)
+ .bloomIndexKeysPerBucket(2)
+ .build())
+ .build();
+ return config;
+ }
+
@Test
public void testLoadUUIDsInMemory() throws IOException {
// Create one RDD of hoodie record
@@ -130,7 +166,7 @@ public class TestHoodieBloomIndex {
@Test
public void testLoadInvolvedFiles() throws IOException {
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).build();
+ HoodieWriteConfig config = makeConfig();
HoodieBloomIndex index = new HoodieBloomIndex(config);
// Create some partitions, and put some files
@@ -188,58 +224,57 @@ public class TestHoodieBloomIndex {
table = HoodieTable.getHoodieTable(metadata, config, jsc);
filesList = index.loadInvolvedFiles(partitions, jsc, table);
assertEquals(filesList.size(), 4);
- // these files will not have the key ranges
- assertNull(filesList.get(0)._2().getMaxRecordKey());
- assertNull(filesList.get(0)._2().getMinRecordKey());
- assertFalse(filesList.get(1)._2().hasKeyRanges());
- assertNotNull(filesList.get(2)._2().getMaxRecordKey());
- assertNotNull(filesList.get(2)._2().getMinRecordKey());
- assertTrue(filesList.get(3)._2().hasKeyRanges());
-
- // no longer sorted, but should have same files.
-
- List<Tuple2<String, BloomIndexFileInfo>> expected = Arrays.asList(
- new Tuple2<>("2016/04/01", new
BloomIndexFileInfo("2_0_20160401010101.parquet")),
- new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("1_0_20150312101010.parquet")),
- new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("3_0_20150312101010.parquet", "000", "000")),
- new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("4_0_20150312101010.parquet", "001", "003")));
- assertEquals(expected, filesList);
+
+ if (rangePruning) {
+ // these files will not have the key ranges
+ assertNull(filesList.get(0)._2().getMaxRecordKey());
+ assertNull(filesList.get(0)._2().getMinRecordKey());
+ assertFalse(filesList.get(1)._2().hasKeyRanges());
+ assertNotNull(filesList.get(2)._2().getMaxRecordKey());
+ assertNotNull(filesList.get(2)._2().getMinRecordKey());
+ assertTrue(filesList.get(3)._2().hasKeyRanges());
+
+ // no longer sorted, but should have same files.
+
+ List<Tuple2<String, BloomIndexFileInfo>> expected = Arrays.asList(
+ new Tuple2<>("2016/04/01", new
BloomIndexFileInfo("2_0_20160401010101.parquet")),
+ new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("1_0_20150312101010.parquet")),
+ new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("3_0_20150312101010.parquet", "000", "000")),
+ new Tuple2<>("2015/03/12", new
BloomIndexFileInfo("4_0_20150312101010.parquet", "001", "003")));
+ assertEquals(expected, filesList);
+ }
}
@Test
public void testRangePruning() {
- for (Boolean rangePruning : new boolean[]{false, true}) {
- Map<String, String> props = new HashMap<>();
- props.put("hoodie.bloom.index.prune.by" + ".ranges",
rangePruning.toString());
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).withProps(props).build();
- HoodieBloomIndex index = new HoodieBloomIndex(config);
-
- final Map<String, List<BloomIndexFileInfo>> partitionToFileIndexInfo =
new HashMap<>();
- partitionToFileIndexInfo.put("2017/10/22", Arrays.asList(new
BloomIndexFileInfo("f1"),
- new BloomIndexFileInfo("f2", "000", "000"), new
BloomIndexFileInfo("f3", "001", "003"),
- new BloomIndexFileInfo("f4", "002", "007"), new
BloomIndexFileInfo("f5", "009", "010")));
-
- JavaPairRDD<String, String> partitionRecordKeyPairRDD =
jsc.parallelize(Arrays.asList(
- new Tuple2<>("2017/10/22", "003"), new Tuple2<>("2017/10/22",
"002"), new Tuple2<>("2017/10/22", "005"),
- new Tuple2<>("2017/10/22", "004"))).mapToPair(t -> t);
-
- List<Tuple2<String, Tuple2<String, HoodieKey>>> comparisonKeyList =
index.explodeRecordRDDWithFileComparisons(
- partitionToFileIndexInfo, partitionRecordKeyPairRDD).collect();
-
- assertEquals(10, comparisonKeyList.size());
- Map<String, List<String>> recordKeyToFileComps =
comparisonKeyList.stream().collect(Collectors.groupingBy(
- t -> t._2()._2().getRecordKey(), Collectors.mapping(t ->
t._2()._1().split("#")[0], Collectors.toList())));
-
- assertEquals(4, recordKeyToFileComps.size());
- assertEquals(new HashSet<>(Arrays.asList("f1", "f3", "f4")), new
HashSet<>(recordKeyToFileComps.get("002")));
- assertEquals(new HashSet<>(Arrays.asList("f1", "f3", "f4")), new
HashSet<>(recordKeyToFileComps.get("003")));
- assertEquals(new HashSet<>(Arrays.asList("f1", "f4")), new
HashSet<>(recordKeyToFileComps.get("004")));
- assertEquals(new HashSet<>(Arrays.asList("f1", "f4")), new
HashSet<>(recordKeyToFileComps.get("005")));
- }
+ HoodieWriteConfig config = makeConfig();
+ HoodieBloomIndex index = new HoodieBloomIndex(config);
+
+ final Map<String, List<BloomIndexFileInfo>> partitionToFileIndexInfo = new
HashMap<>();
+ partitionToFileIndexInfo.put("2017/10/22", Arrays.asList(new
BloomIndexFileInfo("f1"),
+ new BloomIndexFileInfo("f2", "000", "000"), new
BloomIndexFileInfo("f3", "001", "003"),
+ new BloomIndexFileInfo("f4", "002", "007"), new
BloomIndexFileInfo("f5", "009", "010")));
+
+ JavaPairRDD<String, String> partitionRecordKeyPairRDD =
jsc.parallelize(Arrays.asList(
+ new Tuple2<>("2017/10/22", "003"), new Tuple2<>("2017/10/22", "002"),
new Tuple2<>("2017/10/22", "005"),
+ new Tuple2<>("2017/10/22", "004"))).mapToPair(t -> t);
+
+ List<Tuple2<String, Tuple2<String, HoodieKey>>> comparisonKeyList =
index.explodeRecordRDDWithFileComparisons(
+ partitionToFileIndexInfo, partitionRecordKeyPairRDD).collect();
+
+ assertEquals(10, comparisonKeyList.size());
+ Map<String, List<String>> recordKeyToFileComps =
comparisonKeyList.stream().collect(Collectors.groupingBy(
+ t -> t._2()._2().getRecordKey(), Collectors.mapping(t ->
t._2()._1().split("#")[0], Collectors.toList())));
+
+ assertEquals(4, recordKeyToFileComps.size());
+ assertEquals(new HashSet<>(Arrays.asList("f1", "f3", "f4")), new
HashSet<>(recordKeyToFileComps.get("002")));
+ assertEquals(new HashSet<>(Arrays.asList("f1", "f3", "f4")), new
HashSet<>(recordKeyToFileComps.get("003")));
+ assertEquals(new HashSet<>(Arrays.asList("f1", "f4")), new
HashSet<>(recordKeyToFileComps.get("004")));
+ assertEquals(new HashSet<>(Arrays.asList("f1", "f4")), new
HashSet<>(recordKeyToFileComps.get("005")));
}
@Test
- public void testCheckUUIDsAgainstOneFile() throws IOException,
InterruptedException, ClassNotFoundException {
+ public void testCheckUUIDsAgainstOneFile() throws IOException,
InterruptedException {
// Create some records to use
String recordStr1 =
"{\"_row_key\":\"1eb5b87a-1feh-4edd-87b4-6ec96dc405a0\","
@@ -299,7 +334,7 @@ public class TestHoodieBloomIndex {
JavaRDD<HoodieRecord> recordRDD = jsc.emptyRDD();
// Also create the metadata and config
HoodieTableMetaClient metadata = new
HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).build();
+ HoodieWriteConfig config = makeConfig();
HoodieTable table = HoodieTable.getHoodieTable(metadata, config, jsc);
// Let's tag
@@ -315,76 +350,71 @@ public class TestHoodieBloomIndex {
@Test
public void testTagLocation() throws Exception {
- for (Boolean rangePruning : new boolean[]{false, true}) {
- Map<String, String> props = new HashMap<>();
- props.put("hoodie.bloom.index.prune.by" + ".ranges",
rangePruning.toString());
- // We have some records to be tagged (two different partitions)
-
- String rowKey1 = UUID.randomUUID().toString();
- String rowKey2 = UUID.randomUUID().toString();
- String rowKey3 = UUID.randomUUID().toString();
- String rowKey4 = UUID.randomUUID().toString();
- String recordStr1 = "{\"_row_key\":\"" + rowKey1 + "\","
- + "\"time\":\"2016-01-31T03:16:41.415Z\",\"number\":12}";
- String recordStr2 = "{\"_row_key\":\"" + rowKey2 + "\","
- + "\"time\":\"2016-01-31T03:20:41.415Z\",\"number\":100}";
- String recordStr3 = "{\"_row_key\":\"" + rowKey3 + "\","
- + "\"time\":\"2016-01-31T03:16:41.415Z\",\"number\":15}";
- String recordStr4 = "{\"_row_key\":\"" + rowKey4 + "\","
- + "\"time\":\"2015-01-31T03:16:41.415Z\",\"number\":32}";
- TestRawTripPayload rowChange1 = new TestRawTripPayload(recordStr1);
- HoodieRecord record1 = new HoodieRecord(new
HoodieKey(rowChange1.getRowKey(), rowChange1.getPartitionPath()),
- rowChange1);
- TestRawTripPayload rowChange2 = new TestRawTripPayload(recordStr2);
- HoodieRecord record2 = new HoodieRecord(new
HoodieKey(rowChange2.getRowKey(), rowChange2.getPartitionPath()),
- rowChange2);
- TestRawTripPayload rowChange3 = new TestRawTripPayload(recordStr3);
- HoodieRecord record3 = new HoodieRecord(new
HoodieKey(rowChange3.getRowKey(), rowChange3.getPartitionPath()),
- rowChange3);
- TestRawTripPayload rowChange4 = new TestRawTripPayload(recordStr4);
- HoodieRecord record4 = new HoodieRecord(new
HoodieKey(rowChange4.getRowKey(), rowChange4.getPartitionPath()),
- rowChange4);
- JavaRDD<HoodieRecord> recordRDD = jsc.parallelize(Arrays.asList(record1,
record2, record3, record4));
-
- // Also create the metadata and config
- HoodieTableMetaClient metadata = new
HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).withProps(props).build();
- HoodieTable table = HoodieTable.getHoodieTable(metadata, config, jsc);
-
- // Let's tag
- HoodieBloomIndex bloomIndex = new HoodieBloomIndex(config);
- JavaRDD<HoodieRecord> taggedRecordRDD =
bloomIndex.tagLocation(recordRDD, jsc, table);
-
- // Should not find any files
- for (HoodieRecord record : taggedRecordRDD.collect()) {
- assertFalse(record.isCurrentLocationKnown());
- }
+ // We have some records to be tagged (two different partitions)
+ String rowKey1 = UUID.randomUUID().toString();
+ String rowKey2 = UUID.randomUUID().toString();
+ String rowKey3 = UUID.randomUUID().toString();
+ String rowKey4 = UUID.randomUUID().toString();
+ String recordStr1 = "{\"_row_key\":\"" + rowKey1 + "\","
+ + "\"time\":\"2016-01-31T03:16:41.415Z\",\"number\":12}";
+ String recordStr2 = "{\"_row_key\":\"" + rowKey2 + "\","
+ + "\"time\":\"2016-01-31T03:20:41.415Z\",\"number\":100}";
+ String recordStr3 = "{\"_row_key\":\"" + rowKey3 + "\","
+ + "\"time\":\"2016-01-31T03:16:41.415Z\",\"number\":15}";
+ String recordStr4 = "{\"_row_key\":\"" + rowKey4 + "\","
+ + "\"time\":\"2015-01-31T03:16:41.415Z\",\"number\":32}";
+ TestRawTripPayload rowChange1 = new TestRawTripPayload(recordStr1);
+ HoodieRecord record1 = new HoodieRecord(new
HoodieKey(rowChange1.getRowKey(), rowChange1.getPartitionPath()),
+ rowChange1);
+ TestRawTripPayload rowChange2 = new TestRawTripPayload(recordStr2);
+ HoodieRecord record2 = new HoodieRecord(new
HoodieKey(rowChange2.getRowKey(), rowChange2.getPartitionPath()),
+ rowChange2);
+ TestRawTripPayload rowChange3 = new TestRawTripPayload(recordStr3);
+ HoodieRecord record3 = new HoodieRecord(new
HoodieKey(rowChange3.getRowKey(), rowChange3.getPartitionPath()),
+ rowChange3);
+ TestRawTripPayload rowChange4 = new TestRawTripPayload(recordStr4);
+ HoodieRecord record4 = new HoodieRecord(new
HoodieKey(rowChange4.getRowKey(), rowChange4.getPartitionPath()),
+ rowChange4);
+ JavaRDD<HoodieRecord> recordRDD = jsc.parallelize(Arrays.asList(record1,
record2, record3, record4));
+
+ // Also create the metadata and config
+ HoodieTableMetaClient metadata = new
HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
+ HoodieWriteConfig config = makeConfig();
+ HoodieTable table = HoodieTable.getHoodieTable(metadata, config, jsc);
+
+ // Let's tag
+ HoodieBloomIndex bloomIndex = new HoodieBloomIndex(config);
+ JavaRDD<HoodieRecord> taggedRecordRDD = bloomIndex.tagLocation(recordRDD,
jsc, table);
+
+ // Should not find any files
+ for (HoodieRecord record : taggedRecordRDD.collect()) {
+ assertFalse(record.isCurrentLocationKnown());
+ }
- // We create three parquet file, each having one record. (two different
partitions)
- String filename1 =
- HoodieClientTestUtils.writeParquetFile(basePath, "2016/01/31",
Arrays.asList(record1), schema, null, true);
- String filename2 =
- HoodieClientTestUtils.writeParquetFile(basePath, "2016/01/31",
Arrays.asList(record2), schema, null, true);
- String filename3 =
- HoodieClientTestUtils.writeParquetFile(basePath, "2015/01/31",
Arrays.asList(record4), schema, null, true);
-
- // We do the tag again
- metadata = new HoodieTableMetaClient(jsc.hadoopConfiguration(),
basePath);
- table = HoodieTable.getHoodieTable(metadata, config, jsc);
-
- taggedRecordRDD = bloomIndex.tagLocation(recordRDD, jsc, table);
-
- // Check results
- for (HoodieRecord record : taggedRecordRDD.collect()) {
- if (record.getRecordKey().equals(rowKey1)) {
-
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename1)));
- } else if (record.getRecordKey().equals(rowKey2)) {
-
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename2)));
- } else if (record.getRecordKey().equals(rowKey3)) {
- assertTrue(!record.isCurrentLocationKnown());
- } else if (record.getRecordKey().equals(rowKey4)) {
-
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename3)));
- }
+ // We create three parquet file, each having one record. (two different
partitions)
+ String filename1 =
+ HoodieClientTestUtils.writeParquetFile(basePath, "2016/01/31",
Arrays.asList(record1), schema, null, true);
+ String filename2 =
+ HoodieClientTestUtils.writeParquetFile(basePath, "2016/01/31",
Arrays.asList(record2), schema, null, true);
+ String filename3 =
+ HoodieClientTestUtils.writeParquetFile(basePath, "2015/01/31",
Arrays.asList(record4), schema, null, true);
+
+ // We do the tag again
+ metadata = new HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
+ table = HoodieTable.getHoodieTable(metadata, config, jsc);
+
+ taggedRecordRDD = bloomIndex.tagLocation(recordRDD, jsc, table);
+
+ // Check results
+ for (HoodieRecord record : taggedRecordRDD.collect()) {
+ if (record.getRecordKey().equals(rowKey1)) {
+
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename1)));
+ } else if (record.getRecordKey().equals(rowKey2)) {
+
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename2)));
+ } else if (record.getRecordKey().equals(rowKey3)) {
+ assertTrue(!record.isCurrentLocationKnown());
+ } else if (record.getRecordKey().equals(rowKey4)) {
+
assertTrue(record.getCurrentLocation().getFileId().equals(FSUtils.getFileId(filename3)));
}
}
}
@@ -417,7 +447,7 @@ public class TestHoodieBloomIndex {
// Also create the metadata and config
HoodieTableMetaClient metadata = new
HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).build();
+ HoodieWriteConfig config = makeConfig();
HoodieTable table = HoodieTable.getHoodieTable(metadata, config, jsc);
// Let's tag
@@ -490,7 +520,7 @@ public class TestHoodieBloomIndex {
// We do the tag
JavaRDD<HoodieRecord> recordRDD = jsc.parallelize(Arrays.asList(record1,
record2));
HoodieTableMetaClient metadata = new
HoodieTableMetaClient(jsc.hadoopConfiguration(), basePath);
- HoodieWriteConfig config =
HoodieWriteConfig.newBuilder().withPath(basePath).build();
+ HoodieWriteConfig config = makeConfig();
HoodieTable table = HoodieTable.getHoodieTable(metadata, config, jsc);
HoodieBloomIndex bloomIndex = new HoodieBloomIndex(config);