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

lzljs3620320 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 5f18615  TableRead supports reading multiple splits parallely (#11)
5f18615 is described below

commit 5f18615fd6730bb0f99a1c5dbeee743fd95f730d
Author: yuzelin <[email protected]>
AuthorDate: Wed Sep 11 12:01:29 2024 +0800

    TableRead supports reading multiple splits parallely (#11)
---
 java_based_implementation/api_impl.py              |  56 ++++---
 .../org/apache/paimon/python/InvocationUtil.java   |   5 +-
 .../apache/paimon/python/ParallelBytesReader.java  | 171 +++++++++++++++++++++
 .../{BytesReader.java => RecordBytesIterator.java} |  48 ++----
 .../tests/test_write_and_read.py                   |  58 ++++++-
 java_based_implementation/util/constants.py        |   3 +
 java_based_implementation/util/java_utils.py       |   4 +-
 paimon_python_api/catalog.py                       |   2 +-
 paimon_python_api/table_read.py                    |   3 +-
 9 files changed, 286 insertions(+), 64 deletions(-)

diff --git a/java_based_implementation/api_impl.py 
b/java_based_implementation/api_impl.py
index 2605cc3..05487f4 100644
--- a/java_based_implementation/api_impl.py
+++ b/java_based_implementation/api_impl.py
@@ -17,6 +17,7 @@
 
################################################################################
 
 from java_based_implementation.java_gateway import get_gateway
+from java_based_implementation.util import constants
 from java_based_implementation.util.java_utils import to_j_catalog_context, 
check_batch_write
 from paimon_python_api import (catalog, table, read_builder, table_scan, 
split, table_read,
                                write_builder, table_write, commit_message, 
table_commit)
@@ -27,31 +28,33 @@ from typing import List, Iterator
 
 class Catalog(catalog.Catalog):
 
-    def __init__(self, j_catalog):
+    def __init__(self, j_catalog, catalog_options: dict):
         self._j_catalog = j_catalog
+        self._catalog_options = catalog_options
 
     @staticmethod
-    def create(catalog_context: dict) -> 'Catalog':
-        j_catalog_context = to_j_catalog_context(catalog_context)
+    def create(catalog_options: dict) -> 'Catalog':
+        j_catalog_context = to_j_catalog_context(catalog_options)
         gateway = get_gateway()
         j_catalog = gateway.jvm.CatalogFactory.createCatalog(j_catalog_context)
-        return Catalog(j_catalog)
+        return Catalog(j_catalog, catalog_options)
 
     def get_table(self, identifier: str) -> 'Table':
         gateway = get_gateway()
         j_identifier = gateway.jvm.Identifier.fromString(identifier)
         j_table = self._j_catalog.getTable(j_identifier)
-        return Table(j_table)
+        return Table(j_table, self._catalog_options)
 
 
 class Table(table.Table):
 
-    def __init__(self, j_table):
+    def __init__(self, j_table, catalog_options: dict):
         self._j_table = j_table
+        self._catalog_options = catalog_options
 
     def new_read_builder(self) -> 'ReadBuilder':
         j_read_builder = 
get_gateway().jvm.InvocationUtil.getReadBuilder(self._j_table)
-        return ReadBuilder(j_read_builder, self._j_table.rowType())
+        return ReadBuilder(j_read_builder, self._j_table.rowType(), 
self._catalog_options)
 
     def new_batch_write_builder(self) -> 'BatchWriteBuilder':
         check_batch_write(self._j_table)
@@ -61,9 +64,10 @@ class Table(table.Table):
 
 class ReadBuilder(read_builder.ReadBuilder):
 
-    def __init__(self, j_read_builder, j_row_type):
+    def __init__(self, j_read_builder, j_row_type, catalog_options: dict):
         self._j_read_builder = j_read_builder
         self._j_row_type = j_row_type
+        self._catalog_options = catalog_options
 
     def with_projection(self, projection: List[List[int]]) -> 'ReadBuilder':
         self._j_read_builder.withProjection(projection)
@@ -79,7 +83,7 @@ class ReadBuilder(read_builder.ReadBuilder):
 
     def new_read(self) -> 'TableRead':
         j_table_read = self._j_read_builder.newRead()
-        return TableRead(j_table_read, self._j_row_type)
+        return TableRead(j_table_read, self._j_row_type, self._catalog_options)
 
 
 class TableScan(table_scan.TableScan):
@@ -113,24 +117,40 @@ class Split(split.Split):
 
 class TableRead(table_read.TableRead):
 
-    def __init__(self, j_table_read, j_row_type):
+    def __init__(self, j_table_read, j_row_type, catalog_options):
         self._j_table_read = j_table_read
-        self._j_bytes_reader = 
get_gateway().jvm.InvocationUtil.createBytesReader(
-            j_table_read, j_row_type)
+        self._j_row_type = j_row_type
+        self._catalog_options = catalog_options
+        self._j_bytes_reader = None
         self._arrow_schema = None
 
-    def create_reader(self, split: Split):
-        self._j_bytes_reader.setSplit(split.to_j_split())
-        # get schema
+    def create_reader(self, splits):
+        self._init()
+        j_splits = list(map(lambda s: s.to_j_split(), splits))
+        self._j_bytes_reader.setSplits(j_splits)
+        batch_iterator = self._batch_generator()
+        return RecordBatchReader.from_batches(self._arrow_schema, 
batch_iterator)
+
+    def _init(self):
+        if self._j_bytes_reader is None:
+            # get thread num
+            max_workers = self._catalog_options.get(constants.MAX_WORKERS)
+            if max_workers is None:
+                # default is sequential
+                max_workers = 1
+            else:
+                max_workers = int(max_workers)
+            if max_workers <= 0:
+                raise ValueError("max_workers must be greater than 0")
+            self._j_bytes_reader = 
get_gateway().jvm.InvocationUtil.createParallelBytesReader(
+                self._j_table_read, self._j_row_type, max_workers)
+
         if self._arrow_schema is None:
             schema_bytes = self._j_bytes_reader.serializeSchema()
             schema_reader = RecordBatchStreamReader(BufferReader(schema_bytes))
             self._arrow_schema = schema_reader.schema
             schema_reader.close()
 
-        batch_iterator = self._batch_generator()
-        return RecordBatchReader.from_batches(self._arrow_schema, 
batch_iterator)
-
     def _batch_generator(self) -> Iterator[RecordBatch]:
         while True:
             next_bytes = self._j_bytes_reader.next()
diff --git 
a/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/InvocationUtil.java
 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/InvocationUtil.java
index f29773a..61d322a 100644
--- 
a/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/InvocationUtil.java
+++ 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/InvocationUtil.java
@@ -39,8 +39,9 @@ public class InvocationUtil {
         return table.newReadBuilder();
     }
 
-    public static BytesReader createBytesReader(TableRead tableRead, RowType 
rowType) {
-        return new BytesReader(tableRead, rowType);
+    public static ParallelBytesReader createParallelBytesReader(
+            TableRead tableRead, RowType rowType, int threadNum) {
+        return new ParallelBytesReader(tableRead, rowType, threadNum);
     }
 
     public static BytesWriter createBytesWriter(TableWrite tableWrite, RowType 
rowType) {
diff --git 
a/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/ParallelBytesReader.java
 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/ParallelBytesReader.java
new file mode 100644
index 0000000..9f1b390
--- /dev/null
+++ 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/ParallelBytesReader.java
@@ -0,0 +1,171 @@
+/*
+ * 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.paimon.python;
+
+import org.apache.paimon.arrow.ArrowUtils;
+import org.apache.paimon.arrow.vector.ArrowFormatWriter;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.reader.RecordReaderIterator;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ThreadPoolUtils;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.Iterators;
+
+import org.apache.arrow.vector.VectorSchemaRoot;
+
+import javax.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.function.Function;
+
+/** Parallely read Arrow bytes from multiple splits. */
+public class ParallelBytesReader {
+
+    private static final String THREAD_NAME_PREFIX = "PARALLEL_SPLITS_READER";
+    private static final int DEFAULT_WRITE_BATCH_SIZE = 1024;
+
+    private final TableRead tableRead;
+    private final RowType rowType;
+    private final int threadNum;
+
+    private final ConcurrentLinkedQueue<RecordReaderIterator<InternalRow>> 
iterators;
+    private final ConcurrentLinkedQueue<ArrowFormatWriter> arrowFormatWriters;
+
+    private ThreadPoolExecutor executor;
+    private Iterator<byte[]> bytesIterator;
+
+    public ParallelBytesReader(TableRead tableRead, RowType rowType, int 
threadNum) {
+        this.tableRead = tableRead;
+        this.rowType = rowType;
+        this.threadNum = threadNum;
+        this.iterators = new ConcurrentLinkedQueue<>();
+        this.arrowFormatWriters = new ConcurrentLinkedQueue<>();
+    }
+
+    public void setSplits(List<Split> splits) {
+        bytesIterator = randomlyExecute(getExecutor(), makeProcessor(), 
splits);
+    }
+
+    public byte[] serializeSchema() {
+        ArrowFormatWriter arrowFormatWriter = newWriter();
+        VectorSchemaRoot vsr = arrowFormatWriter.getVectorSchemaRoot();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ArrowUtils.serializeToIpc(vsr, out);
+        arrowFormatWriter.close();
+        return out.toByteArray();
+    }
+
+    @Nullable
+    public byte[] next() {
+        if (bytesIterator.hasNext()) {
+            return bytesIterator.next();
+        } else {
+            closeResources();
+            return null;
+        }
+    }
+
+    private ThreadPoolExecutor getExecutor() {
+        if (executor == null) {
+            executor = ThreadPoolUtils.createCachedThreadPool(threadNum, 
THREAD_NAME_PREFIX);
+        }
+        return executor;
+    }
+
+    private Function<Split, Iterator<byte[]>> makeProcessor() {
+        return split -> {
+            try {
+                RecordReader<InternalRow> recordReader = 
tableRead.createReader(split);
+                RecordReaderIterator<InternalRow> iterator =
+                        new RecordReaderIterator<>(recordReader);
+                iterators.add(iterator);
+                ArrowFormatWriter arrowFormatWriter = newWriter();
+                arrowFormatWriters.add(arrowFormatWriter);
+                return new RecordBytesIterator(iterator, arrowFormatWriter);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        };
+    }
+
+    private <U, T> Iterator<T> randomlyExecute(
+            ExecutorService executor, Function<U, Iterator<T>> processor, 
Collection<U> input) {
+        List<Future<Iterator<T>>> futures = new ArrayList<>(input.size());
+        for (U u : input) {
+            futures.add(executor.submit(() -> processor.apply(u)));
+        }
+        return futuresToIterIter(futures);
+    }
+
+    private <T> Iterator<T> futuresToIterIter(List<Future<Iterator<T>>> 
futures) {
+        final Queue<Future<Iterator<T>>> queue = new ArrayDeque<>(futures);
+        return Iterators.concat(
+                new Iterator<Iterator<T>>() {
+                    public boolean hasNext() {
+                        return !queue.isEmpty();
+                    }
+
+                    public Iterator<T> next() {
+                        try {
+                            return queue.poll().get();
+                        } catch (InterruptedException e) {
+                            Thread.currentThread().interrupt();
+                            throw new RuntimeException(e);
+                        } catch (ExecutionException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }
+                });
+    }
+
+    private void closeResources() {
+        for (RecordReaderIterator<InternalRow> iterator : iterators) {
+            try {
+                iterator.close();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        iterators.clear();
+
+        for (ArrowFormatWriter arrowFormatWriter : arrowFormatWriters) {
+            arrowFormatWriter.close();
+        }
+        arrowFormatWriters.clear();
+    }
+
+    private ArrowFormatWriter newWriter() {
+        return new ArrowFormatWriter(rowType, DEFAULT_WRITE_BATCH_SIZE, true);
+    }
+}
diff --git 
a/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/BytesReader.java
 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/RecordBytesIterator.java
similarity index 57%
rename from 
java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/BytesReader.java
rename to 
java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/RecordBytesIterator.java
index 9272f98..d4965db 100644
--- 
a/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/BytesReader.java
+++ 
b/java_based_implementation/paimon-python-java-bridge/src/main/java/org/apache/paimon/python/RecordBytesIterator.java
@@ -21,54 +21,33 @@ package org.apache.paimon.python;
 import org.apache.paimon.arrow.ArrowUtils;
 import org.apache.paimon.arrow.vector.ArrowFormatWriter;
 import org.apache.paimon.data.InternalRow;
-import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.reader.RecordReaderIterator;
-import org.apache.paimon.table.source.Split;
-import org.apache.paimon.table.source.TableRead;
-import org.apache.paimon.types.RowType;
 
 import org.apache.arrow.vector.VectorSchemaRoot;
 
-import javax.annotation.Nullable;
-
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-/** Read Arrow bytes from split. */
-public class BytesReader {
+import java.util.Iterator;
 
-    private static final int DEFAULT_WRITE_BATCH_SIZE = 2048;
+/** Cast a {@link RecordReaderIterator} to bytes iterator. */
+public class RecordBytesIterator implements Iterator<byte[]> {
 
-    private final TableRead tableRead;
+    private final RecordReaderIterator<InternalRow> iterator;
     private final ArrowFormatWriter arrowFormatWriter;
 
-    private RecordReaderIterator<InternalRow> iterator;
     private InternalRow nextRow;
 
-    public BytesReader(TableRead tableRead, RowType rowType) {
-        this.tableRead = tableRead;
-        this.arrowFormatWriter = new ArrowFormatWriter(rowType, 
DEFAULT_WRITE_BATCH_SIZE, true);
-    }
-
-    public void setSplit(Split split) throws IOException {
-        RecordReader<InternalRow> recordReader = tableRead.createReader(split);
-        iterator = new RecordReaderIterator<InternalRow>(recordReader);
+    public RecordBytesIterator(
+            RecordReaderIterator<InternalRow> iterator, ArrowFormatWriter 
arrowFormatWriter) {
+        this.iterator = iterator;
+        this.arrowFormatWriter = arrowFormatWriter;
         nextRow();
     }
 
-    public byte[] serializeSchema() {
-        VectorSchemaRoot vsr = arrowFormatWriter.getVectorSchemaRoot();
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ArrowUtils.serializeToIpc(vsr, out);
-        return out.toByteArray();
+    public boolean hasNext() {
+        return nextRow != null;
     }
 
-    @Nullable
-    public byte[] next() throws Exception {
-        if (nextRow == null) {
-            return null;
-        }
-
+    public byte[] next() {
         int rowCount = 0;
         while (nextRow != null && arrowFormatWriter.write(nextRow)) {
             nextRow();
@@ -80,11 +59,6 @@ public class BytesReader {
         vsr.setRowCount(rowCount);
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ArrowUtils.serializeToIpc(vsr, out);
-        if (nextRow == null) {
-            // close resource
-            arrowFormatWriter.close();
-            iterator.close();
-        }
         return out.toByteArray();
     }
 
diff --git a/java_based_implementation/tests/test_write_and_read.py 
b/java_based_implementation/tests/test_write_and_read.py
index 4511825..0ea834e 100644
--- a/java_based_implementation/tests/test_write_and_read.py
+++ b/java_based_implementation/tests/test_write_and_read.py
@@ -83,7 +83,7 @@ class TableWriteReadTest(unittest.TestCase):
         table_commit.close()
 
         # read data
-        table = Table(j_table)
+        table = Table(j_table, {})
         read_builder = table.new_read_builder()
         table_scan = read_builder.new_scan()
         table_read = read_builder.new_read()
@@ -92,7 +92,7 @@ class TableWriteReadTest(unittest.TestCase):
         data_frames = [
             batch.to_pandas()
             for split in splits
-            for batch in table_read.create_reader(split)
+            for batch in table_read.create_reader([split])
         ]
         self.assertEqual(len(data_frames), 0)
 
@@ -132,7 +132,7 @@ class TableWriteReadTest(unittest.TestCase):
         data_frames = [
             batch.to_pandas()
             for split in splits
-            for batch in table_read.create_reader(split)
+            for batch in table_read.create_reader([split])
         ]
         result = pd.concat(data_frames)
 
@@ -184,3 +184,55 @@ class TableWriteReadTest(unittest.TestCase):
         self.assertEqual(
             str(e.exception),
             "Doesn't support writing dynamic bucket or cross partition table.")
+
+    def testParallelRead(self):
+        create_simple_table(self.warehouse, 'default', 'test_parallel_read', 
False)
+
+        catalog = Catalog.create({'warehouse': self.warehouse, 'max-workers': 
'2'})
+        table = catalog.get_table('default.test_parallel_read')
+
+        # prepare data
+        n_times = 4
+        expected_data = {
+            'f0': [],
+            'f1': []
+        }
+        for i in range(n_times):
+            data = {
+                'f0': [i],
+                'f1': [str(i * 2)]
+            }
+            expected_data['f0'].append(i)
+            expected_data['f1'].append(str(i * 2))
+
+            df = pd.DataFrame(data)
+            df['f0'] = df['f0'].astype('int32')
+            record_batch = pa.RecordBatch.from_pandas(df)
+
+            # write and commit data
+            write_builder = table.new_batch_write_builder()
+            table_write = write_builder.new_write()
+            table_commit = write_builder.new_commit()
+
+            table_write.write(record_batch)
+            commit_messages = table_write.prepare_commit()
+            table_commit.commit(commit_messages)
+
+        # read data parallely
+        read_builder = table.new_read_builder()
+        table_scan = read_builder.new_scan()
+        table_read = read_builder.new_read()
+        splits = table_scan.plan().splits()
+
+        data_frames = [
+            batch.to_pandas()
+            for batch in table_read.create_reader(splits)
+        ]
+        result = pd.concat(data_frames)
+
+        expected_df = pd.DataFrame(expected_data)
+        expected_df['f0'] = expected_df['f0'].astype('int32')
+
+        # check data (ignore index)
+        pd.testing.assert_frame_equal(
+            result.reset_index(drop=True), expected_df.reset_index(drop=True))
diff --git a/java_based_implementation/util/constants.py 
b/java_based_implementation/util/constants.py
index 71ca0e6..7e8923a 100644
--- a/java_based_implementation/util/constants.py
+++ b/java_based_implementation/util/constants.py
@@ -23,3 +23,6 @@ PYPAIMON_JAVA_CLASSPATH = '_PYPAIMON_JAVA_CLASSPATH'
 PYPAIMON_MAIN_CLASS = 'org.apache.paimon.python.PythonGatewayServer'
 PYPAIMON_MAIN_ARGS = '_PYPAIMON_MAIN_ARGS'
 PYPAIMON_TOX_TEST = '_PYPAIMON_TOX_TEST'
+
+# ------------------------ for catalog options ------------------------
+MAX_WORKERS = "max-workers"
diff --git a/java_based_implementation/util/java_utils.py 
b/java_based_implementation/util/java_utils.py
index b9f2523..1f0d1ec 100644
--- a/java_based_implementation/util/java_utils.py
+++ b/java_based_implementation/util/java_utils.py
@@ -19,9 +19,9 @@
 from java_based_implementation.java_gateway import get_gateway
 
 
-def to_j_catalog_context(catalog_context: dict):
+def to_j_catalog_context(catalog_options: dict):
     gateway = get_gateway()
-    j_options = gateway.jvm.Options(catalog_context)
+    j_options = gateway.jvm.Options(catalog_options)
     return gateway.jvm.CatalogContext.create(j_options)
 
 
diff --git a/paimon_python_api/catalog.py b/paimon_python_api/catalog.py
index 1a78444..7fbacc5 100644
--- a/paimon_python_api/catalog.py
+++ b/paimon_python_api/catalog.py
@@ -28,7 +28,7 @@ class Catalog(ABC):
 
     @staticmethod
     @abstractmethod
-    def create(catalog_context: dict) -> 'Catalog':
+    def create(catalog_options: dict) -> 'Catalog':
         """Create catalog from configuration."""
 
     @abstractmethod
diff --git a/paimon_python_api/table_read.py b/paimon_python_api/table_read.py
index c17b49d..47b8aae 100644
--- a/paimon_python_api/table_read.py
+++ b/paimon_python_api/table_read.py
@@ -19,11 +19,12 @@
 from abc import ABC, abstractmethod
 from pyarrow import RecordBatchReader
 from paimon_python_api.split import Split
+from typing import List
 
 
 class TableRead(ABC):
     """To read data from data splits."""
 
     @abstractmethod
-    def create_reader(self, split: Split) -> RecordBatchReader:
+    def create_reader(self, splits: List[Split]) -> RecordBatchReader:
         """Return a reader containing batches of pyarrow format."""

Reply via email to