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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 63a7b529c5 [Feature][Connector-V2] Maxcompute Source Round-Robin Split 
Assignment (#11131)
63a7b529c5 is described below

commit 63a7b529c588de68d88cfdfd6ee148480f772d02
Author: zhiliang-wu <[email protected]>
AuthorDate: Tue Jun 23 15:27:24 2026 +0200

    [Feature][Connector-V2] Maxcompute Source Round-Robin Split Assignment 
(#11131)
    
    Co-authored-by: WU Zhiliang (External) <[email protected]>
---
 .../source/MaxcomputeSourceSplitEnumerator.java    |  61 +++++----
 .../source/MaxcomputeSourceSplitTest.java          | 151 +++++++++++++++++----
 2 files changed, 162 insertions(+), 50 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-maxcompute/src/main/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitEnumerator.java
 
b/seatunnel-connectors-v2/connector-maxcompute/src/main/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitEnumerator.java
index 0f582bdd37..4dbd5abbd5 100644
--- 
a/seatunnel-connectors-v2/connector-maxcompute/src/main/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitEnumerator.java
+++ 
b/seatunnel-connectors-v2/connector-maxcompute/src/main/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitEnumerator.java
@@ -32,6 +32,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -108,37 +109,51 @@ public class MaxcomputeSourceSplitEnumerator
     @Override
     public void handleSplitRequest(int subtaskId) {}
 
+    // visible for testing
+    static Set<MaxcomputeSourceSplit> computeSplits(
+            int numReaders,
+            Collection<SourceTableInfo> sourceTableInfos,
+            Map<TablePath, Long> tableRecordCounts) {
+        Set<MaxcomputeSourceSplit> allSplit = new LinkedHashSet<>();
+        int chunkIndex = 0;
+        for (SourceTableInfo sourceTableInfo : sourceTableInfos) {
+            TablePath tablePath = 
sourceTableInfo.getCatalogTable().getTablePath();
+            long recordCount = tableRecordCounts.get(tablePath);
+            int splitRow = MaxcomputeSourceOptions.SPLIT_ROW.defaultValue();
+            if (sourceTableInfo.getSplitRow() != null && 
sourceTableInfo.getSplitRow() > 0) {
+                splitRow = sourceTableInfo.getSplitRow();
+            }
+            for (long num = 0; num < recordCount; num += splitRow) {
+                int ownerReader = chunkIndex % numReaders;
+                allSplit.add(
+                        new MaxcomputeSourceSplit(
+                                num,
+                                Math.min((long) splitRow, recordCount - num),
+                                tablePath,
+                                ownerReader));
+                chunkIndex++;
+            }
+        }
+        return allSplit;
+    }
+
     private void discoverySplits() throws TunnelException {
         int numReaders = enumeratorContext.currentParallelism();
-        Set<MaxcomputeSourceSplit> allSplit = new HashSet<>();
+        Map<TablePath, Long> tableRecordCounts = new HashMap<>();
         for (SourceTableInfo sourceTableInfo : sourceTableInfos.values()) {
-            Set<MaxcomputeSourceSplit> splits = new HashSet<>();
             TableTunnel.DownloadSession session =
                     MaxcomputeUtil.getDownloadSession(
                             readonlyConfig,
                             sourceTableInfo.getCatalogTable().getTablePath(),
                             sourceTableInfo.getPartitionSpec());
-            long recordCount = session.getRecordCount();
-            int splitRowNum = (int) Math.ceil((double) recordCount / 
numReaders);
-            int splitRow = MaxcomputeSourceOptions.SPLIT_ROW.defaultValue();
-            if (sourceTableInfo.getSplitRow() != null && 
sourceTableInfo.getSplitRow() > 0) {
-                splitRow = sourceTableInfo.getSplitRow();
-            }
-            for (int i = 0; i < numReaders; i++) {
-                int readerStart = i * splitRowNum;
-                int readerEnd = (int) Math.min((i + 1) * splitRowNum, 
recordCount);
-                for (int num = readerStart; num < readerEnd; num += splitRow) {
-                    splits.add(
-                            new MaxcomputeSourceSplit(
-                                    num,
-                                    Math.min(splitRow, readerEnd - num),
-                                    
sourceTableInfo.getCatalogTable().getTablePath(),
-                                    i));
-                }
-            }
-            assignedSplits.forEach(splits::remove);
-            allSplit.addAll(splits);
+            tableRecordCounts.put(
+                    sourceTableInfo.getCatalogTable().getTablePath(), 
session.getRecordCount());
         }
+
+        Set<MaxcomputeSourceSplit> allSplit =
+                computeSplits(numReaders, sourceTableInfos.values(), 
tableRecordCounts);
+        assignedSplits.forEach(allSplit::remove);
+
         addSplitChangeToPendingAssignments(allSplit);
         log.debug("Assigned {} to {} readers.", allSplit, numReaders);
         log.info("Calculated splits successfully, the size of splits is {}.", 
allSplit.size());
@@ -147,7 +162,7 @@ public class MaxcomputeSourceSplitEnumerator
     private void 
addSplitChangeToPendingAssignments(Collection<MaxcomputeSourceSplit> newSplits) 
{
         for (MaxcomputeSourceSplit split : newSplits) {
             int ownerReader = split.getIndex() % 
enumeratorContext.currentParallelism();
-            pendingSplits.computeIfAbsent(ownerReader, r -> new 
HashSet<>()).add(split);
+            pendingSplits.computeIfAbsent(ownerReader, r -> new 
LinkedHashSet<>()).add(split);
         }
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-maxcompute/src/test/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitTest.java
 
b/seatunnel-connectors-v2/connector-maxcompute/src/test/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitTest.java
index d6dfcf6eba..83aae67404 100644
--- 
a/seatunnel-connectors-v2/connector-maxcompute/src/test/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitTest.java
+++ 
b/seatunnel-connectors-v2/connector-maxcompute/src/test/java/org/apache/seatunnel/connectors/seatunnel/maxcompute/source/MaxcomputeSourceSplitTest.java
@@ -17,14 +17,22 @@
 
 package org.apache.seatunnel.connectors.seatunnel.maxcompute.source;
 
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.catalog.TablePath;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 public class MaxcomputeSourceSplitTest {
 
     @Test
@@ -33,42 +41,131 @@ public class MaxcomputeSourceSplitTest {
         long recordCount = 105000;
         int splitRow = 10000;
 
-        int splitRowNum = (int) Math.ceil((double) recordCount / numReaders); 
// 21000
         TablePath tablePath = TablePath.of("test_db", "test_schema", 
"test_table");
 
+        Map<TablePath, Long> tableRecordCounts = new HashMap<>();
+        tableRecordCounts.put(tablePath, recordCount);
+
+        CatalogTable catalogTable = mock(CatalogTable.class);
+        when(catalogTable.getTablePath()).thenReturn(tablePath);
+
+        SourceTableInfo tableInfo = new SourceTableInfo(catalogTable, null, 
splitRow);
+        List<SourceTableInfo> tableInfos = new ArrayList<>();
+        tableInfos.add(tableInfo);
+
+        Set<MaxcomputeSourceSplit> splits =
+                MaxcomputeSourceSplitEnumerator.computeSplits(
+                        numReaders, tableInfos, tableRecordCounts);
+
         Set<String> splitIds = new HashSet<>();
         int[] indexCounts = new int[numReaders];
-        int totalSplits = 0;
-
-        // Simulate the inner logic of 
MaxcomputeSourceSplitEnumerator.discoverySplits
-        for (int i = 0; i < numReaders; i++) {
-            int readerStart = i * splitRowNum;
-            int readerEnd = (int) Math.min((i + 1) * splitRowNum, recordCount);
-            for (int num = readerStart; num < readerEnd; num += splitRow) {
-                MaxcomputeSourceSplit split =
-                        new MaxcomputeSourceSplit(
-                                num, Math.min(splitRow, readerEnd - num), 
tablePath, i);
-
-                // Verify splitId uniqueness
-                Assertions.assertTrue(
-                        splitIds.add(split.splitId()),
-                        "Duplicate splitId found: " + split.splitId());
 
-                // Verify index assigned to the split matches the reader index
-                Assertions.assertEquals(i, split.getIndex());
+        for (MaxcomputeSourceSplit split : splits) {
+            Assertions.assertTrue(
+                    splitIds.add(split.splitId()), "Duplicate splitId found: " 
+ split.splitId());
+            indexCounts[split.getIndex()]++;
+        }
 
-                indexCounts[i]++;
-                totalSplits++;
-            }
+        Assertions.assertEquals(11, splits.size());
+
+        // Verify index distribution (round-robin):
+        // 11 splits / 5 readers = 2 splits each, plus 1 remainder for reader 
0.
+        Assertions.assertEquals(3, indexCounts[0], "Reader 0 split count 
mismatch!");
+        Assertions.assertEquals(2, indexCounts[1], "Reader 1 split count 
mismatch!");
+        Assertions.assertEquals(2, indexCounts[2], "Reader 2 split count 
mismatch!");
+        Assertions.assertEquals(2, indexCounts[3], "Reader 3 split count 
mismatch!");
+        Assertions.assertEquals(2, indexCounts[4], "Reader 4 split count 
mismatch!");
+    }
+
+    @Test
+    public void testMultiTableRoundRobinDistribution() {
+        int numReaders = 3;
+        int numTables = 5;
+        long recordCountPerTable = 15000;
+        int splitRow = 10000;
+
+        Map<TablePath, Long> tableRecordCounts = new HashMap<>();
+        List<SourceTableInfo> tableInfos = new ArrayList<>();
+
+        for (int t = 0; t < numTables; t++) {
+            TablePath tablePath = TablePath.of("test_db", "test_schema", 
"test_table_" + t);
+            tableRecordCounts.put(tablePath, recordCountPerTable);
+
+            CatalogTable catalogTable = mock(CatalogTable.class);
+            when(catalogTable.getTablePath()).thenReturn(tablePath);
+
+            SourceTableInfo tableInfo = new SourceTableInfo(catalogTable, 
null, splitRow);
+            tableInfos.add(tableInfo);
         }
 
-        Assertions.assertTrue(totalSplits == 15);
+        Set<MaxcomputeSourceSplit> splits =
+                MaxcomputeSourceSplitEnumerator.computeSplits(
+                        numReaders, tableInfos, tableRecordCounts);
+
+        int[] indexCounts = new int[numReaders];
 
-        // Verify index distribution:
-        // Record count 105000 / 5 readers = 21000 splits per reader.
-        // Split row is 10000. Loop steps: 0, 10000, 20000. So 3 splits per 
reader.
-        for (int i = 0; i < numReaders; i++) {
-            Assertions.assertEquals(3, indexCounts[i], "Reader " + i + " split 
count mismatch!");
+        for (MaxcomputeSourceSplit split : splits) {
+            indexCounts[split.getIndex()]++;
         }
+
+        Assertions.assertEquals(10, splits.size());
+
+        // 10 splits / 3 readers = 3 splits each, plus 1 remainder for reader 
0.
+        Assertions.assertEquals(4, indexCounts[0], "Reader 0 split count 
mismatch!");
+        Assertions.assertEquals(3, indexCounts[1], "Reader 1 split count 
mismatch!");
+        Assertions.assertEquals(3, indexCounts[2], "Reader 2 split count 
mismatch!");
+    }
+
+    @Test
+    public void testComputeSplitsAscendingRowStartOrder() {
+        int numReaders = 3;
+        int numTables = 2;
+        Map<TablePath, Long> tableRecordCounts = new HashMap<>();
+        List<SourceTableInfo> tableInfos = new ArrayList<>();
+
+        for (int i = 0; i < numTables; i++) {
+            TablePath path = TablePath.of("db", "schema", "table" + i);
+            // 105,000 rows / 10,000 splitRow = 11 splits per table.
+            // With 3 readers, each reader gets 3-4 splits PER TABLE.
+            tableRecordCounts.put(path, 105000L);
+
+            CatalogTable catalogTable = mock(CatalogTable.class);
+            when(catalogTable.getTablePath()).thenReturn(path);
+
+            SourceTableInfo tableInfo = new SourceTableInfo(catalogTable, 
null, 10000);
+            tableInfos.add(tableInfo);
+        }
+
+        // Drive the actual production logic
+        Set<MaxcomputeSourceSplit> splits =
+                MaxcomputeSourceSplitEnumerator.computeSplits(
+                        numReaders, tableInfos, tableRecordCounts);
+
+        // Guard against HashSet regression: splits iterated from the Set MUST 
be in insertion
+        // order,
+        // which means for any given reader and table, the rowStart must be 
strictly ascending.
+        Map<Integer, Map<TablePath, Long>> lastRowStarts = new HashMap<>();
+
+        for (MaxcomputeSourceSplit split : splits) {
+            int reader = split.getIndex();
+            TablePath path = split.getTablePath();
+            long rowStart = split.getRowStart();
+
+            lastRowStarts.computeIfAbsent(reader, r -> new HashMap<>());
+            Long lastStart = lastRowStarts.get(reader).get(path);
+
+            if (lastStart != null) {
+                Assertions.assertTrue(
+                        rowStart > lastStart,
+                        "HashSet Regression! Splits for reader "
+                                + reader
+                                + " and table "
+                                + path
+                                + " are not in ascending order!");
+            }
+            lastRowStarts.get(reader).put(path, rowStart);
+        }
+
+        Assertions.assertEquals(22, splits.size(), "Should have exactly 22 
splits total");
     }
 }

Reply via email to