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

jiabaosun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-connector-mongodb.git


The following commit(s) were added to refs/heads/main by this push:
     new fd177d7  [FLINK-36075][Connectors/MongoDB] Add pagination partitioning 
strategy (#45)
fd177d7 is described below

commit fd177d71b705da3e2749077d1b860eb3d14979f6
Author: yuxiqian <[email protected]>
AuthorDate: Mon Dec 2 09:23:08 2024 +0800

    [FLINK-36075][Connectors/MongoDB] Add pagination partitioning strategy (#45)
---
 .../mongodb/common/utils/MongoConstants.java       |   3 +
 .../mongodb/source/config/MongoReadOptions.java    |  29 ++-
 .../splitter/MongoPaginationSplitter.java          | 126 ++++++++++
 .../enumerator/splitter/MongoSampleSplitter.java   |   8 +-
 .../enumerator/splitter/MongoSingleSplitter.java   |  11 +-
 .../splitter/MongoSplitVectorSplitter.java         |   4 +-
 .../source/enumerator/splitter/MongoSplitters.java |   2 +
 .../enumerator/splitter/PartitionStrategy.java     |   5 +
 .../mongodb/table/MongoConnectorOptions.java       |  21 +-
 .../mongodb/table/MongoDynamicTableFactory.java    |   1 +
 .../mongodb/table/config/MongoConfiguration.java   |   5 +
 .../MongoSourceEnumStateSerializerTest.java        |   4 +-
 .../splitter/MongoPaginationSplitterTest.java      | 268 +++++++++++++++++++++
 .../splitter/MongoSampleSplitterTest.java          |  14 +-
 14 files changed, 470 insertions(+), 31 deletions(-)

diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/common/utils/MongoConstants.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/common/utils/MongoConstants.java
index e60b453..378d6db 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/common/utils/MongoConstants.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/common/utils/MongoConstants.java
@@ -65,6 +65,9 @@ public class MongoConstants {
 
     public static final BsonDocument ID_HINT = new BsonDocument(ID_FIELD, new 
BsonInt32(1));
 
+    public static final BsonDocument BSON_MIN_BOUNDARY = new 
BsonDocument(ID_FIELD, BSON_MIN_KEY);
+    public static final BsonDocument BSON_MAX_BOUNDARY = new 
BsonDocument(ID_FIELD, BSON_MAX_KEY);
+
     public static final JsonWriterSettings DEFAULT_JSON_WRITER_SETTINGS =
             JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build();
 
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/config/MongoReadOptions.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/config/MongoReadOptions.java
index fb990ed..41590e1 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/config/MongoReadOptions.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/config/MongoReadOptions.java
@@ -22,6 +22,8 @@ import org.apache.flink.configuration.MemorySize;
 import 
org.apache.flink.connector.mongodb.source.enumerator.splitter.PartitionStrategy;
 import 
org.apache.flink.connector.mongodb.source.reader.split.MongoScanSourceSplitReader;
 
+import javax.annotation.Nullable;
+
 import java.io.Serializable;
 import java.util.Objects;
 
@@ -49,17 +51,21 @@ public class MongoReadOptions implements Serializable {
 
     private final int samplesPerPartition;
 
+    private final @Nullable Integer partitionRecordSize;
+
     private MongoReadOptions(
             int fetchSize,
             boolean noCursorTimeout,
             PartitionStrategy partitionStrategy,
             MemorySize partitionSize,
-            int samplesPerPartition) {
+            int samplesPerPartition,
+            @Nullable Integer partitionRecordSize) {
         this.fetchSize = fetchSize;
         this.noCursorTimeout = noCursorTimeout;
         this.partitionStrategy = partitionStrategy;
         this.partitionSize = partitionSize;
         this.samplesPerPartition = samplesPerPartition;
+        this.partitionRecordSize = partitionRecordSize;
     }
 
     public int getFetchSize() {
@@ -82,6 +88,10 @@ public class MongoReadOptions implements Serializable {
         return samplesPerPartition;
     }
 
+    public @Nullable Integer getPartitionRecordSize() {
+        return partitionRecordSize;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -114,6 +124,7 @@ public class MongoReadOptions implements Serializable {
         private PartitionStrategy partitionStrategy = 
SCAN_PARTITION_STRATEGY.defaultValue();
         private MemorySize partitionSize = SCAN_PARTITION_SIZE.defaultValue();
         private int samplesPerPartition = 
SCAN_PARTITION_SAMPLES.defaultValue();
+        private @Nullable Integer partitionRecordSize = null;
 
         private MongoReadOptionsBuilder() {}
 
@@ -199,6 +210,19 @@ public class MongoReadOptions implements Serializable {
             return this;
         }
 
+        /**
+         * Sets the number of records in each partition. This will only take 
effect when the
+         * partition strategy is set to Pagination.
+         *
+         * @param partitionRecordSize number of records in each partition.
+         * @return this builder
+         */
+        public MongoReadOptionsBuilder setPartitionRecordSize(
+                @Nullable Integer partitionRecordSize) {
+            this.partitionRecordSize = partitionRecordSize;
+            return this;
+        }
+
         /**
          * Build the {@link MongoReadOptions}.
          *
@@ -210,7 +234,8 @@ public class MongoReadOptions implements Serializable {
                     noCursorTimeout,
                     partitionStrategy,
                     partitionSize,
-                    samplesPerPartition);
+                    samplesPerPartition,
+                    partitionRecordSize);
         }
     }
 }
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitter.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitter.java
new file mode 100644
index 0000000..64cb530
--- /dev/null
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitter.java
@@ -0,0 +1,126 @@
+/*
+ * 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.flink.connector.mongodb.source.enumerator.splitter;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.connector.mongodb.source.config.MongoReadOptions;
+import org.apache.flink.connector.mongodb.source.split.MongoScanSourceSplit;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.client.model.Aggregates;
+import com.mongodb.client.model.Projections;
+import com.mongodb.client.model.Sorts;
+import org.bson.BsonDocument;
+import org.bson.conversions.Bson;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
+
+/** Mongo Splitter that splits MongoDB collection evenly by record counts. */
+@Internal
+public class MongoPaginationSplitter {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(MongoPaginationSplitter.class);
+
+    public static Collection<MongoScanSourceSplit> split(MongoSplitContext 
splitContext) {
+        MongoReadOptions readOptions = splitContext.getReadOptions();
+        MongoNamespace namespace = splitContext.getMongoNamespace();
+
+        // If partition record size isn't present, we'll use the partition 
size option and average
+        // object size to calculate number of records in each partitioned 
split.
+        Integer partitionRecordSize = readOptions.getPartitionRecordSize();
+        if (partitionRecordSize == null) {
+            long avgObjSizeInBytes = splitContext.getAvgObjSize();
+            if (avgObjSizeInBytes == 0) {
+                LOG.info(
+                        "{} seems to be an empty collection, Returning a 
single partition.",
+                        namespace);
+                return MongoSingleSplitter.split(splitContext);
+            }
+
+            partitionRecordSize =
+                    Math.toIntExact(readOptions.getPartitionSize().getBytes() 
/ avgObjSizeInBytes);
+        }
+
+        long totalNumOfDocuments = splitContext.getCount();
+
+        if (partitionRecordSize >= totalNumOfDocuments) {
+            LOG.info(
+                    "Fewer documents ({}) than the number of documents per 
partition ({}), Returning a single partition.",
+                    totalNumOfDocuments,
+                    partitionRecordSize);
+            return MongoSingleSplitter.split(splitContext);
+        }
+
+        int numberOfPartitions =
+                (int) (Math.ceil(totalNumOfDocuments / (double) 
partitionRecordSize));
+
+        BsonDocument lastUpperBound = null;
+        List<MongoScanSourceSplit> paginatedSplits = new ArrayList<>();
+
+        for (int splitNum = 0; splitNum < numberOfPartitions; splitNum++) {
+            List<Bson> pipeline = new ArrayList<>();
+
+            pipeline.add(Aggregates.project(Projections.include(ID_FIELD)));
+            pipeline.add(Aggregates.project(Sorts.ascending(ID_FIELD)));
+
+            // We don't have to set the upper bounds limit if we're generating 
the first split.
+            if (lastUpperBound != null) {
+                BsonDocument matchFilter = new BsonDocument();
+                if (lastUpperBound.containsKey(ID_FIELD)) {
+                    matchFilter.put(
+                            ID_FIELD, new BsonDocument("$gte", 
lastUpperBound.get(ID_FIELD)));
+                }
+                pipeline.add(Aggregates.match(matchFilter));
+            }
+            pipeline.add(Aggregates.skip(partitionRecordSize));
+            pipeline.add(Aggregates.limit(1));
+
+            BsonDocument currentUpperBound =
+                    splitContext
+                            .getMongoCollection()
+                            .aggregate(pipeline)
+                            .allowDiskUse(true)
+                            .first();
+
+            paginatedSplits.add(
+                    new MongoScanSourceSplit(
+                            String.format("%s_%d", namespace, splitNum),
+                            namespace.getDatabaseName(),
+                            namespace.getCollectionName(),
+                            lastUpperBound != null ? lastUpperBound : 
BSON_MIN_BOUNDARY,
+                            currentUpperBound != null ? currentUpperBound : 
BSON_MAX_BOUNDARY,
+                            ID_HINT));
+
+            if (currentUpperBound == null) {
+                break;
+            }
+            lastUpperBound = currentUpperBound;
+        }
+
+        return paginatedSplits;
+    }
+}
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitter.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitter.java
index 22ca113..ce094d7 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitter.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitter.java
@@ -36,8 +36,8 @@ import java.util.Collection;
 import java.util.List;
 import java.util.function.BiFunction;
 
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_KEY;
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_KEY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_BOUNDARY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
 
@@ -109,10 +109,10 @@ public class MongoSampleSplitter {
     @VisibleForTesting
     static List<MongoScanSourceSplit> createSplits(
             List<BsonDocument> samples, int samplesPerPartition, 
MongoNamespace namespace) {
-        samples.add(new BsonDocument(ID_FIELD, BSON_MAX_KEY));
+        samples.add(BSON_MAX_BOUNDARY);
 
         List<MongoScanSourceSplit> sourceSplits = new ArrayList<>();
-        BsonDocument partitionStart = new BsonDocument(ID_FIELD, BSON_MIN_KEY);
+        BsonDocument partitionStart = BSON_MIN_BOUNDARY;
         int splitNum = 0;
         for (int i = samplesPerPartition - 1; i < samples.size(); i += 
samplesPerPartition) {
             sourceSplits.add(createSplit(namespace, splitNum++, 
partitionStart, samples.get(i)));
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSingleSplitter.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSingleSplitter.java
index f177a0d..d8cb777 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSingleSplitter.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSingleSplitter.java
@@ -20,14 +20,11 @@ package 
org.apache.flink.connector.mongodb.source.enumerator.splitter;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.connector.mongodb.source.split.MongoScanSourceSplit;
 
-import org.bson.BsonDocument;
-
 import java.util.Collection;
 
 import static java.util.Collections.singletonList;
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_KEY;
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_KEY;
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_BOUNDARY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
 
 /** Mongo Splitter that splits MongoDB collection as a single split. */
@@ -42,8 +39,8 @@ public class MongoSingleSplitter {
                         splitContext.getMongoNamespace().getFullName(),
                         splitContext.getDatabaseName(),
                         splitContext.getCollectionName(),
-                        new BsonDocument(ID_FIELD, BSON_MIN_KEY),
-                        new BsonDocument(ID_FIELD, BSON_MAX_KEY),
+                        BSON_MIN_BOUNDARY,
+                        BSON_MAX_BOUNDARY,
                         ID_HINT);
 
         return singletonList(singleSplit);
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitVectorSplitter.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitVectorSplitter.java
index 055c9a3..1cd60e6 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitVectorSplitter.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitVectorSplitter.java
@@ -38,7 +38,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_KEY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_KEY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
@@ -90,7 +90,7 @@ public class MongoSplitVectorSplitter {
         }
 
         // Complete right bound: (lastKey, maxKey)
-        splitKeys.add(new BsonDocument(ID_FIELD, BSON_MAX_KEY));
+        splitKeys.add(BSON_MAX_BOUNDARY);
 
         List<MongoScanSourceSplit> sourceSplits = new 
ArrayList<>(splitKeys.size());
 
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitters.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitters.java
index 1ebad81..d2ef346 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitters.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSplitters.java
@@ -64,6 +64,8 @@ public class MongoSplitters {
                 return MongoSplitVectorSplitter.split(splitContext);
             case SHARDED:
                 return MongoShardedSplitter.split(splitContext);
+            case PAGINATION:
+                return MongoPaginationSplitter.split(splitContext);
             case DEFAULT:
             default:
                 return splitContext.isSharded()
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/PartitionStrategy.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/PartitionStrategy.java
index a73de04..c117c34 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/PartitionStrategy.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/PartitionStrategy.java
@@ -55,6 +55,11 @@ public enum PartitionStrategy implements DescribedEnum {
             text(
                     "Read the chunk ranges from config.chunks collection and 
splits to multiple chunks. Only support sharded collections.")),
 
+    PAGINATION(
+            "pagination",
+            text(
+                    "Creating chunk records evenly by count. Each chunk will 
have exactly the same number of records.")),
+
     DEFAULT(
             "default",
             text(
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoConnectorOptions.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoConnectorOptions.java
index a579eaa..36948f2 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoConnectorOptions.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoConnectorOptions.java
@@ -75,12 +75,13 @@ public class MongoConnectorOptions {
                     .enumType(PartitionStrategy.class)
                     .defaultValue(PartitionStrategy.DEFAULT)
                     .withDescription(
-                            "Specifies the partition strategy. Available 
strategies are single, sample, split-vector, sharded and default."
-                                    + "The single partition strategy treats 
the entire collection as a single partition."
-                                    + "The sample partition strategy samples 
the collection and generate partitions which is fast but possibly uneven."
-                                    + "The split-vector partition strategy 
uses the splitVector command to generate partitions for non-sharded collections 
which is fast and even. The splitVector permission is required."
-                                    + "The sharded partition strategy reads 
config.chunks (MongoDB splits a sharded collection into chunks, and the range 
of the chunks are stored within the collection) as the partitions directly."
-                                    + "The sharded partition strategy is only 
used for sharded collection which is fast and even. Read permission of config 
database is required."
+                            "Specifies the partition strategy. Available 
strategies are single, sample, split-vector, sharded, pagination and default. "
+                                    + "The single partition strategy treats 
the entire collection as a single partition. "
+                                    + "The sample partition strategy samples 
the collection and generate partitions which is fast but possibly uneven. "
+                                    + "The split-vector partition strategy 
uses the splitVector command to generate partitions for non-sharded collections 
which is fast and even. The splitVector permission is required. "
+                                    + "The sharded partition strategy reads 
config.chunks (MongoDB splits a sharded collection into chunks, and the range 
of the chunks are stored within the collection) as the partitions directly. "
+                                    + "The sharded partition strategy is only 
used for sharded collection which is fast and even. Read permission of config 
database is required. "
+                                    + "The pagination partition strategy 
splits records evenly. Each chunk will have exactly the same number of records. 
This could be configured by `scan.partition.record-size` option. "
                                     + "The default partition strategy uses 
sharded strategy for sharded collections otherwise using split vector 
strategy.");
 
     public static final ConfigOption<MemorySize> SCAN_PARTITION_SIZE =
@@ -99,6 +100,14 @@ public class MongoConnectorOptions {
                                     + "Then uses every 
'scan.partition.samples' as the value to use to calculate the partition 
boundaries."
                                     + "The total number of samples taken is 
calculated as: samples per partition * (count of documents / number of 
documents per partition.");
 
+    public static final ConfigOption<Integer> SCAN_PARTITION_RECORD_SIZE =
+            ConfigOptions.key("scan.partition.record-size")
+                    .intType()
+                    .noDefaultValue()
+                    .withDescription(
+                            "Specifies the number of records in each chunk. 
Only takes effect when `scan.partition.strategy` is `pagination`. "
+                                    + "This option will be automatically 
inferred from `scan.partition.size` if absent.");
+
     public static final ConfigOption<Duration> LOOKUP_RETRY_INTERVAL =
             ConfigOptions.key("lookup.retry.interval")
                     .durationType()
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoDynamicTableFactory.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoDynamicTableFactory.java
index 055c6b7..6bf87d2 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoDynamicTableFactory.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/MongoDynamicTableFactory.java
@@ -179,6 +179,7 @@ public class MongoDynamicTableFactory
                 .setNoCursorTimeout(configuration.isNoCursorTimeout())
                 .setPartitionStrategy(configuration.getPartitionStrategy())
                 .setPartitionSize(configuration.getPartitionSize())
+                .setPartitionRecordSize(configuration.getPartitionRecordSize())
                 .setSamplesPerPartition(configuration.getSamplesPerPartition())
                 .build();
     }
diff --git 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/config/MongoConfiguration.java
 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/config/MongoConfiguration.java
index 753b429..1a6bd51 100644
--- 
a/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/config/MongoConfiguration.java
+++ 
b/flink-connector-mongodb/src/main/java/org/apache/flink/connector/mongodb/table/config/MongoConfiguration.java
@@ -38,6 +38,7 @@ import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.FIL
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.LOOKUP_RETRY_INTERVAL;
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_CURSOR_NO_TIMEOUT;
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_FETCH_SIZE;
+import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_PARTITION_RECORD_SIZE;
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_PARTITION_SAMPLES;
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_PARTITION_SIZE;
 import static 
org.apache.flink.connector.mongodb.table.MongoConnectorOptions.SCAN_PARTITION_STRATEGY;
@@ -90,6 +91,10 @@ public class MongoConfiguration {
         return config.get(SCAN_PARTITION_SAMPLES);
     }
 
+    public Integer getPartitionRecordSize() {
+        return config.get(SCAN_PARTITION_RECORD_SIZE);
+    }
+
     // -----------------------------------Lookup 
Config----------------------------------------
     public int getLookupMaxRetries() {
         return config.get(LookupOptions.MAX_RETRIES);
diff --git 
a/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/MongoSourceEnumStateSerializerTest.java
 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/MongoSourceEnumStateSerializerTest.java
index 8f0a796..54da962 100644
--- 
a/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/MongoSourceEnumStateSerializerTest.java
+++ 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/MongoSourceEnumStateSerializerTest.java
@@ -17,7 +17,6 @@
 
 package org.apache.flink.connector.mongodb.source.enumerator;
 
-import org.apache.flink.connector.mongodb.common.utils.MongoConstants;
 import org.apache.flink.connector.mongodb.source.split.MongoScanSourceSplit;
 
 import org.bson.BsonDocument;
@@ -29,6 +28,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
 import static 
org.apache.flink.connector.mongodb.source.enumerator.MongoSourceEnumStateSerializer.INSTANCE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -75,7 +75,7 @@ class MongoSourceEnumStateSerializerTest {
                 "db",
                 "coll",
                 new BsonDocument("_id", new BsonInt32(index)),
-                new BsonDocument("_id", MongoConstants.BSON_MAX_KEY),
+                BSON_MAX_BOUNDARY,
                 ID_HINT);
     }
 }
diff --git 
a/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitterTest.java
 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitterTest.java
new file mode 100644
index 0000000..e234422
--- /dev/null
+++ 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoPaginationSplitterTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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.flink.connector.mongodb.source.enumerator.splitter;
+
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.MemorySize;
+import org.apache.flink.connector.mongodb.source.config.MongoReadOptions;
+import org.apache.flink.connector.mongodb.source.split.MongoScanSourceSplit;
+import org.apache.flink.connector.mongodb.testutils.MongoShardedContainers;
+import org.apache.flink.connector.mongodb.testutils.MongoTestUtil;
+
+import com.mongodb.MongoNamespace;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import org.bson.BsonDocument;
+import org.bson.BsonInt64;
+import org.bson.BsonString;
+import org.bson.BsonValue;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.containers.Network;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_KEY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_KEY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_HINT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link MongoPaginationSplitter}. */
+class MongoPaginationSplitterTest {
+
+    @RegisterExtension
+    private static final MongoShardedContainers MONGO_SHARDED_CONTAINER =
+            MongoTestUtil.createMongoDBShardedContainers(Network.newNetwork());
+
+    private static MongoClient mongoClient;
+
+    private static final MongoNamespace TEST_NS = new 
MongoNamespace("test.test");
+
+    private static final int TOTAL_RECORDS_COUNT = 120;
+
+    @BeforeAll
+    static void beforeAll() {
+        mongoClient = 
MongoClients.create(MONGO_SHARDED_CONTAINER.getConnectionString());
+        MongoCollection<BsonDocument> coll =
+                mongoClient
+                        .getDatabase(TEST_NS.getDatabaseName())
+                        .getCollection(TEST_NS.getCollectionName())
+                        .withDocumentClass(BsonDocument.class);
+        coll.insertMany(initializeRecords());
+    }
+
+    @AfterAll
+    static void afterAll() {
+        if (mongoClient != null) {
+            mongoClient.close();
+        }
+    }
+
+    ///  Test cases that specifies number of records in each partition 
explicitly.
+    @Test
+    void testSingleSplitPartitions() {
+        MongoSplitContext splitContext = 
createSplitContext(TOTAL_RECORDS_COUNT);
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(SINGLE_SPLIT);
+    }
+
+    @Test
+    void testLargePartitionRecordSize() {
+        MongoSplitContext splitContext = 
createSplitContext(TOTAL_RECORDS_COUNT * 2);
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(SINGLE_SPLIT);
+    }
+
+    @Test
+    void testLargerSizedPartitions() {
+        MongoSplitContext splitContext = createSplitContext(15);
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(
+                        createReferenceSplits(
+                                Arrays.asList(
+                                        Tuple2.of(BSON_MIN_KEY, new 
BsonInt64(15)),
+                                        Tuple2.of(new BsonInt64(15), new 
BsonInt64(30)),
+                                        Tuple2.of(new BsonInt64(30), new 
BsonInt64(45)),
+                                        Tuple2.of(new BsonInt64(45), new 
BsonInt64(60)),
+                                        Tuple2.of(new BsonInt64(60), new 
BsonInt64(75)),
+                                        Tuple2.of(new BsonInt64(75), new 
BsonInt64(90)),
+                                        Tuple2.of(new BsonInt64(90), new 
BsonInt64(105)),
+                                        Tuple2.of(new BsonInt64(105), 
BSON_MAX_KEY))));
+    }
+
+    @Test
+    void testOffByOnePartitions() {
+        {
+            MongoSplitContext splitContext = 
createSplitContext(TOTAL_RECORDS_COUNT - 1);
+            assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                    .isEqualTo(
+                            createReferenceSplits(
+                                    Arrays.asList(
+                                            Tuple2.of(
+                                                    BSON_MIN_KEY,
+                                                    new 
BsonInt64(TOTAL_RECORDS_COUNT - 1)),
+                                            Tuple2.of(
+                                                    new 
BsonInt64(TOTAL_RECORDS_COUNT - 1),
+                                                    BSON_MAX_KEY))));
+        }
+
+        {
+            MongoSplitContext splitContext = 
createSplitContext(TOTAL_RECORDS_COUNT);
+            assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                    .isEqualTo(SINGLE_SPLIT);
+        }
+    }
+
+    ///  Test cases that do not specify number of records, and estimates 
record size with
+    /// `avgObjSize`.
+    @Test
+    void testEstimatedSingleSplitPartitions() {
+        MongoSplitContext splitContext =
+                createSplitContext(MemorySize.ofMebiBytes(16), 
MemorySize.ZERO);
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(SINGLE_SPLIT);
+    }
+
+    @Test
+    void testEstimatedLargerSizedPartitions() {
+        MongoSplitContext splitContext =
+                createSplitContext(MemorySize.ofMebiBytes(50), 
MemorySize.ofMebiBytes(3));
+
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(
+                        createReferenceSplits(
+                                Arrays.asList(
+                                        Tuple2.of(BSON_MIN_KEY, new 
BsonInt64(16)),
+                                        Tuple2.of(new BsonInt64(16), new 
BsonInt64(32)),
+                                        Tuple2.of(new BsonInt64(32), new 
BsonInt64(48)),
+                                        Tuple2.of(new BsonInt64(48), new 
BsonInt64(64)),
+                                        Tuple2.of(new BsonInt64(64), new 
BsonInt64(80)),
+                                        Tuple2.of(new BsonInt64(80), new 
BsonInt64(96)),
+                                        Tuple2.of(new BsonInt64(96), new 
BsonInt64(112)),
+                                        Tuple2.of(new BsonInt64(112), 
BSON_MAX_KEY))));
+    }
+
+    @Test
+    void testEstimatedOffByOnePartitions() {
+        {
+            MongoSplitContext splitContext =
+                    createSplitContext(
+                            MemorySize.ofMebiBytes(TOTAL_RECORDS_COUNT - 1),
+                            MemorySize.ofMebiBytes(1));
+            assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                    .isEqualTo(
+                            createReferenceSplits(
+                                    Arrays.asList(
+                                            Tuple2.of(
+                                                    BSON_MIN_KEY,
+                                                    new 
BsonInt64(TOTAL_RECORDS_COUNT - 1)),
+                                            Tuple2.of(
+                                                    new 
BsonInt64(TOTAL_RECORDS_COUNT - 1),
+                                                    BSON_MAX_KEY))));
+        }
+
+        {
+            MongoSplitContext splitContext =
+                    createSplitContext(
+                            MemorySize.ofMebiBytes(TOTAL_RECORDS_COUNT), 
MemorySize.ofMebiBytes(1));
+            assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                    .isEqualTo(SINGLE_SPLIT);
+        }
+    }
+
+    @Test
+    void testEstimateWithoutAvgObjSize() {
+        MongoSplitContext splitContext =
+                createSplitContext(MemorySize.ofMebiBytes(1), MemorySize.ZERO);
+        assertThat(new 
ArrayList<>(MongoPaginationSplitter.split(splitContext)))
+                .isEqualTo(SINGLE_SPLIT);
+    }
+
+    private static List<BsonDocument> initializeRecords() {
+        return IntStream.range(0, 
MongoPaginationSplitterTest.TOTAL_RECORDS_COUNT)
+                .mapToObj(
+                        idx ->
+                                new BsonDocument("_id", new BsonInt64(idx))
+                                        .append(
+                                                "str",
+                                                new 
BsonString(String.format("Record #%d", idx))))
+                .collect(Collectors.toList());
+    }
+
+    private static MongoSplitContext createSplitContext(
+            MemorySize partitionSize, MemorySize avgObjSize) {
+        long avgObjSizeInBytes = avgObjSize.getBytes();
+        return new MongoSplitContext(
+                
MongoReadOptions.builder().setPartitionSize(partitionSize).build(),
+                mongoClient,
+                TEST_NS,
+                false,
+                TOTAL_RECORDS_COUNT,
+                (long) TOTAL_RECORDS_COUNT * avgObjSizeInBytes,
+                avgObjSizeInBytes);
+    }
+
+    private static MongoSplitContext createSplitContext(int 
partitionRecordSize) {
+        return new MongoSplitContext(
+                
MongoReadOptions.builder().setPartitionRecordSize(partitionRecordSize).build(),
+                mongoClient,
+                TEST_NS,
+                false,
+                MongoPaginationSplitterTest.TOTAL_RECORDS_COUNT,
+                0,
+                0);
+    }
+
+    private static List<MongoScanSourceSplit> createReferenceSplits(
+            List<Tuple2<BsonValue, BsonValue>> ranges) {
+
+        List<MongoScanSourceSplit> results = new ArrayList<>();
+        for (int i = 0; i < ranges.size(); i++) {
+            results.add(
+                    new MongoScanSourceSplit(
+                            TEST_NS.getFullName() + "_" + i,
+                            TEST_NS.getDatabaseName(),
+                            TEST_NS.getCollectionName(),
+                            new BsonDocument(ID_FIELD, ranges.get(i).f0),
+                            new BsonDocument(ID_FIELD, ranges.get(i).f1),
+                            ID_HINT));
+        }
+        return results;
+    }
+
+    private static final List<MongoScanSourceSplit> SINGLE_SPLIT =
+            Collections.singletonList(
+                    new MongoScanSourceSplit(
+                            TEST_NS.getFullName(),
+                            TEST_NS.getDatabaseName(),
+                            TEST_NS.getCollectionName(),
+                            BSON_MIN_BOUNDARY,
+                            BSON_MAX_BOUNDARY,
+                            ID_HINT));
+}
diff --git 
a/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitterTest.java
 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitterTest.java
index ce4de54..55e0a3b 100644
--- 
a/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitterTest.java
+++ 
b/flink-connector-mongodb/src/test/java/org/apache/flink/connector/mongodb/source/enumerator/splitter/MongoSampleSplitterTest.java
@@ -29,8 +29,8 @@ import org.junit.jupiter.api.Test;
 import java.util.ArrayList;
 import java.util.List;
 
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_KEY;
-import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_KEY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MAX_BOUNDARY;
+import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.BSON_MIN_BOUNDARY;
 import static 
org.apache.flink.connector.mongodb.common.utils.MongoConstants.ID_FIELD;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -38,8 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat;
 class MongoSampleSplitterTest {
 
     private static final MongoNamespace TEST_NS = new 
MongoNamespace("test.test");
-    private static final BsonDocument MIN = new BsonDocument(ID_FIELD, 
BSON_MIN_KEY);
-    private static final BsonDocument MAX = new BsonDocument(ID_FIELD, 
BSON_MAX_KEY);
 
     @Test
     void testSplitEmptyCollection() {
@@ -124,7 +122,7 @@ class MongoSampleSplitterTest {
         assertThat(splits.get(0))
                 .satisfies(
                         split -> {
-                            assertThat(split.getMin()).isEqualTo(MIN);
+                            
assertThat(split.getMin()).isEqualTo(BSON_MIN_BOUNDARY);
                             
assertThat(split.getMax()).isEqualTo(samples.get(1));
                         });
         assertThat(splits.get(1))
@@ -137,7 +135,7 @@ class MongoSampleSplitterTest {
                 .satisfies(
                         split -> {
                             
assertThat(split.getMin()).isEqualTo(samples.get(3));
-                            assertThat(split.getMax()).isEqualTo(MAX);
+                            
assertThat(split.getMax()).isEqualTo(BSON_MAX_BOUNDARY);
                         });
     }
 
@@ -151,7 +149,7 @@ class MongoSampleSplitterTest {
 
     private static void assertSingleSplit(List<MongoScanSourceSplit> splits) {
         assertThat(splits.size()).isEqualTo(1);
-        assertThat(splits.get(0).getMin()).isEqualTo(MIN);
-        assertThat(splits.get(0).getMax()).isEqualTo(MAX);
+        assertThat(splits.get(0).getMin()).isEqualTo(BSON_MIN_BOUNDARY);
+        assertThat(splits.get(0).getMax()).isEqualTo(BSON_MAX_BOUNDARY);
     }
 }


Reply via email to