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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 49ffa3f9dc [python] Avoid concurrent import failures during split 
deserialization (#9250)
49ffa3f9dc is described below

commit 49ffa3f9dc9f324033421cf94018e0762e2d7247
Author: XiaoHongbo <[email protected]>
AuthorDate: Mon Aug 17 12:18:39 2026 +0800

    [python] Avoid concurrent import failures during split deserialization 
(#9250)
---
 paimon-python/pypaimon/__init__.py                 |  44 ++++-
 paimon-python/pypaimon/globalindex/__init__.py     | 113 +++++++++----
 paimon-python/pypaimon/tag/__init__.py             |  32 +++-
 .../pypaimon/tests/concurrent_import_test.py       | 187 +++++++++++++++++++++
 4 files changed, 336 insertions(+), 40 deletions(-)

diff --git a/paimon-python/pypaimon/__init__.py 
b/paimon-python/pypaimon/__init__.py
index aa18f812e7..017dfa5303 100644
--- a/paimon-python/pypaimon/__init__.py
+++ b/paimon-python/pypaimon/__init__.py
@@ -15,7 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import importlib
 import sys
+import threading
 
 if sys.version_info[:2] < (3, 8):
     try:
@@ -23,11 +25,13 @@ if sys.version_info[:2] < (3, 8):
     except ImportError:
         pass
 
-from pypaimon.catalog.catalog_factory import CatalogFactory
-from pypaimon.filesystem.pvfs import PaimonVirtualFileSystem
-from pypaimon.schema.schema import Schema
-from pypaimon.tag.tag import Tag
-from pypaimon.tag.tag_manager import TagManager
+if sys.version_info[:2] < (3, 7):
+    # Module-level __getattr__ is unavailable before Python 3.7.
+    from pypaimon.catalog.catalog_factory import CatalogFactory
+    from pypaimon.filesystem.pvfs import PaimonVirtualFileSystem
+    from pypaimon.schema.schema import Schema
+    from pypaimon.tag.tag import Tag
+    from pypaimon.tag.tag_manager import TagManager
 
 __all__ = [
     "PaimonVirtualFileSystem",
@@ -38,9 +42,31 @@ __all__ = [
     "SQLContext",
 ]
 
+_LAZY_EXPORTS = {
+    "CatalogFactory": ("pypaimon.catalog.catalog_factory", "CatalogFactory"),
+    "PaimonVirtualFileSystem": (
+        "pypaimon.filesystem.pvfs", "PaimonVirtualFileSystem",
+    ),
+    "Schema": ("pypaimon.schema.schema", "Schema"),
+    "Tag": ("pypaimon.tag.tag", "Tag"),
+    "TagManager": ("pypaimon.tag.tag_manager", "TagManager"),
+    "SQLContext": ("pypaimon_rust.datafusion", "SQLContext"),
+}
+
+# Unsynchronized lazy imports from two threads can acquire module locks in
+# opposite orders (pypaimon.tag initializes both tag and tag_manager) and
+# fail with _DeadlockError; the eager imports previously serialized this on
+# the root module lock.
+_LAZY_IMPORT_LOCK = threading.RLock()
+
 
 def __getattr__(name):
-    if name == "SQLContext":
-        from pypaimon_rust.datafusion import SQLContext
-        return SQLContext
-    raise AttributeError("module 'pypaimon' has no attribute {}".format(name))
+    target = _LAZY_EXPORTS.get(name)
+    if target is None:
+        raise AttributeError(
+            "module 'pypaimon' has no attribute {}".format(name))
+    with _LAZY_IMPORT_LOCK:
+        if name not in globals():
+            module = importlib.import_module(target[0])
+            globals()[name] = getattr(module, target[1])
+        return globals()[name]
diff --git a/paimon-python/pypaimon/globalindex/__init__.py 
b/paimon-python/pypaimon/globalindex/__init__.py
index b9ef7c9a28..377b0a900a 100644
--- a/paimon-python/pypaimon/globalindex/__init__.py
+++ b/paimon-python/pypaimon/globalindex/__init__.py
@@ -15,35 +15,41 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from pypaimon.globalindex.global_index_result import GlobalIndexResult
-from pypaimon.globalindex.global_index_reader import GlobalIndexReader, 
FieldRef
-from pypaimon.globalindex.vector_search import VectorSearch
-from pypaimon.globalindex.full_text_search import FullTextSearch
-from pypaimon.globalindex.vector_search_result import (
-    ScoredGlobalIndexResult,
-    DictBasedScoredIndexResult,
-    ScoreGetter,
-)
-from pypaimon.globalindex.global_index_meta import GlobalIndexMeta, 
GlobalIndexIOMeta
-from pypaimon.globalindex.global_index_evaluator import GlobalIndexEvaluator
-from pypaimon.globalindex.data_evolution_global_index_scanner import (
-    DataEvolutionGlobalIndexScanner,
-)
-from pypaimon.globalindex.key_serializer import KeySerializer
-from pypaimon.globalindex.memory_slice_input import MemorySliceInput
-from pypaimon.globalindex.offset_global_index_reader import 
OffsetGlobalIndexReader
-from pypaimon.globalindex.sorted_file_global_index_reader import 
SortedFileGlobalIndexReader
-from pypaimon.globalindex.sorted_file_meta_selector import 
SortedFileMetaSelector
-from pypaimon.globalindex.sorted_index_file_meta import SortedIndexFileMeta
-from pypaimon.globalindex.create_global_index import (
-    GlobalIndexBuilder,
-    create_global_index,
-)
-from pypaimon.globalindex.drop_global_index import (
-    GlobalIndexDropper,
-    drop_global_index,
-)
-from pypaimon.utils.range import Range
+import importlib
+import sys
+import threading
+
+if sys.version_info[:2] < (3, 7):
+    # Module-level __getattr__ is unavailable before Python 3.7.
+    from pypaimon.globalindex.global_index_result import GlobalIndexResult
+    from pypaimon.globalindex.global_index_reader import GlobalIndexReader, 
FieldRef
+    from pypaimon.globalindex.vector_search import VectorSearch
+    from pypaimon.globalindex.full_text_search import FullTextSearch
+    from pypaimon.globalindex.vector_search_result import (
+        ScoredGlobalIndexResult,
+        DictBasedScoredIndexResult,
+        ScoreGetter,
+    )
+    from pypaimon.globalindex.global_index_meta import GlobalIndexMeta, 
GlobalIndexIOMeta
+    from pypaimon.globalindex.global_index_evaluator import 
GlobalIndexEvaluator
+    from pypaimon.globalindex.data_evolution_global_index_scanner import (
+        DataEvolutionGlobalIndexScanner,
+    )
+    from pypaimon.globalindex.key_serializer import KeySerializer
+    from pypaimon.globalindex.memory_slice_input import MemorySliceInput
+    from pypaimon.globalindex.offset_global_index_reader import 
OffsetGlobalIndexReader
+    from pypaimon.globalindex.sorted_file_global_index_reader import 
SortedFileGlobalIndexReader
+    from pypaimon.globalindex.sorted_file_meta_selector import 
SortedFileMetaSelector
+    from pypaimon.globalindex.sorted_index_file_meta import SortedIndexFileMeta
+    from pypaimon.globalindex.create_global_index import (
+        GlobalIndexBuilder,
+        create_global_index,
+    )
+    from pypaimon.globalindex.drop_global_index import (
+        GlobalIndexDropper,
+        drop_global_index,
+    )
+    from pypaimon.utils.range import Range
 
 __all__ = [
     'GlobalIndexResult',
@@ -70,3 +76,52 @@ __all__ = [
     'drop_global_index',
     'Range',
 ]
+
+_MODULE_BY_EXPORT = {
+    'GlobalIndexResult': 'pypaimon.globalindex.global_index_result',
+    'GlobalIndexReader': 'pypaimon.globalindex.global_index_reader',
+    'FieldRef': 'pypaimon.globalindex.global_index_reader',
+    'VectorSearch': 'pypaimon.globalindex.vector_search',
+    'FullTextSearch': 'pypaimon.globalindex.full_text_search',
+    'ScoredGlobalIndexResult': 'pypaimon.globalindex.vector_search_result',
+    'DictBasedScoredIndexResult': 'pypaimon.globalindex.vector_search_result',
+    'ScoreGetter': 'pypaimon.globalindex.vector_search_result',
+    'GlobalIndexMeta': 'pypaimon.globalindex.global_index_meta',
+    'GlobalIndexIOMeta': 'pypaimon.globalindex.global_index_meta',
+    'GlobalIndexEvaluator': 'pypaimon.globalindex.global_index_evaluator',
+    'DataEvolutionGlobalIndexScanner':
+        'pypaimon.globalindex.data_evolution_global_index_scanner',
+    'KeySerializer': 'pypaimon.globalindex.key_serializer',
+    'MemorySliceInput': 'pypaimon.globalindex.memory_slice_input',
+    'OffsetGlobalIndexReader':
+        'pypaimon.globalindex.offset_global_index_reader',
+    'SortedFileGlobalIndexReader':
+        'pypaimon.globalindex.sorted_file_global_index_reader',
+    'SortedFileMetaSelector':
+        'pypaimon.globalindex.sorted_file_meta_selector',
+    'SortedIndexFileMeta': 'pypaimon.globalindex.sorted_index_file_meta',
+    'GlobalIndexBuilder': 'pypaimon.globalindex.create_global_index',
+    'create_global_index': 'pypaimon.globalindex.create_global_index',
+    'GlobalIndexDropper': 'pypaimon.globalindex.drop_global_index',
+    'drop_global_index': 'pypaimon.globalindex.drop_global_index',
+    'Range': 'pypaimon.utils.range',
+}
+
+# Eagerly importing the exports above builds a circular chain: submodules
+# such as index_file_meta initialize this package on first import, while
+# create_global_index and the scanner chain import those submodules back.
+# Lazy resolution keeps this package init trivial; the lock serializes
+# first-time imports racing from multiple threads.
+_LAZY_IMPORT_LOCK = threading.RLock()
+
+
+def __getattr__(name):
+    module_name = _MODULE_BY_EXPORT.get(name)
+    if module_name is None:
+        raise AttributeError(
+            "module 'pypaimon.globalindex' has no attribute {}".format(name))
+    with _LAZY_IMPORT_LOCK:
+        if name not in globals():
+            module = importlib.import_module(module_name)
+            globals()[name] = getattr(module, name)
+        return globals()[name]
diff --git a/paimon-python/pypaimon/tag/__init__.py 
b/paimon-python/pypaimon/tag/__init__.py
index 8f51bf5602..384c154797 100644
--- a/paimon-python/pypaimon/tag/__init__.py
+++ b/paimon-python/pypaimon/tag/__init__.py
@@ -15,7 +15,35 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from pypaimon.tag.tag import Tag
-from pypaimon.tag.tag_manager import TagManager
+import importlib
+import sys
+import threading
+
+if sys.version_info[:2] < (3, 7):
+    # Module-level __getattr__ is unavailable before Python 3.7.
+    from pypaimon.tag.tag import Tag
+    from pypaimon.tag.tag_manager import TagManager
 
 __all__ = ["Tag", "TagManager"]
+
+_MODULE_BY_EXPORT = {
+    "Tag": "pypaimon.tag.tag",
+    "TagManager": "pypaimon.tag.tag_manager",
+}
+
+# Eagerly importing both siblings here lets two threads acquire the module
+# locks of tag and tag_manager in opposite orders and fail with
+# _DeadlockError; lazy resolution keeps this package init trivial.
+_LAZY_IMPORT_LOCK = threading.RLock()
+
+
+def __getattr__(name):
+    module_name = _MODULE_BY_EXPORT.get(name)
+    if module_name is None:
+        raise AttributeError(
+            "module 'pypaimon.tag' has no attribute {}".format(name))
+    with _LAZY_IMPORT_LOCK:
+        if name not in globals():
+            module = importlib.import_module(module_name)
+            globals()[name] = getattr(module, name)
+        return globals()[name]
diff --git a/paimon-python/pypaimon/tests/concurrent_import_test.py 
b/paimon-python/pypaimon/tests/concurrent_import_test.py
new file mode 100644
index 0000000000..a1ccb428ae
--- /dev/null
+++ b/paimon-python/pypaimon/tests/concurrent_import_test.py
@@ -0,0 +1,187 @@
+# 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.
+
+import base64
+import os
+import pickle
+import subprocess
+import sys
+import unittest
+
+from pypaimon.manifest.schema.data_file_meta import DataFileMeta
+from pypaimon.manifest.schema.simple_stats import SimpleStats
+from pypaimon.read.query_auth_split import QueryAuthSplit
+from pypaimon.read.split import DataSplit
+from pypaimon.table.row.generic_row import GenericRow
+
+
+class ConcurrentImportTest(unittest.TestCase):
+
+    @unittest.skipIf(
+        sys.version_info[:2] < (3, 7),
+        "module-level lazy attributes require Python 3.7+",
+    )
+    def test_concurrent_query_auth_split_deserialization(self):
+        empty = GenericRow([], [])
+        file_meta = DataFileMeta.create(
+            file_name="data.parquet",
+            file_size=1,
+            row_count=1,
+            min_key=empty,
+            max_key=empty,
+            key_stats=SimpleStats.empty_stats(),
+            value_stats=SimpleStats.empty_stats(),
+            min_sequence_number=0,
+            max_sequence_number=0,
+            schema_id=0,
+            level=0,
+            extra_files=[],
+            first_row_id=0,
+        )
+        payload = base64.b64encode(pickle.dumps(QueryAuthSplit(
+            DataSplit([file_meta], empty, 0, snapshot_id=1), None,
+        ))).decode("ascii")
+        script = r"""
+import base64
+import importlib
+import pickle
+import sys
+import threading
+
+payload = base64.b64decode(sys.argv[1])
+barrier = threading.Barrier(24)
+errors = []
+modules = [
+    "pypaimon.manifest.schema.data_file_meta",
+    "pypaimon.manifest.schema.simple_stats",
+    "pypaimon.read.query_auth_split",
+]
+
+
+def deserialize(index):
+    try:
+        barrier.wait()
+        if index < len(modules):
+            importlib.import_module(modules[index])
+        for _ in range(20):
+            pickle.loads(payload)
+    except BaseException as error:
+        errors.append(repr(error))
+
+
+threads = [
+    threading.Thread(target=deserialize, args=(index,))
+    for index in range(24)
+]
+for thread in threads:
+    thread.start()
+for thread in threads:
+    thread.join()
+if errors:
+    print("\n".join(errors))
+    raise SystemExit(1)
+"""
+        result = subprocess.run(
+            [sys.executable, "-c", script, payload],
+            env=os.environ.copy(),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+            timeout=30,
+        )
+        self.assertEqual(0, result.returncode, result.stdout)
+
+    @unittest.skipIf(
+        sys.version_info[:2] < (3, 7),
+        "module-level lazy attributes require Python 3.7+",
+    )
+    def test_concurrent_top_level_lazy_exports(self):
+        script = r"""
+import sys
+import threading
+
+sys.setswitchinterval(1e-6)
+thread_count = 16
+barrier = threading.Barrier(thread_count)
+errors = []
+statements = ["from pypaimon import Tag", "from pypaimon import TagManager"]
+
+
+def resolve(statement):
+    try:
+        barrier.wait()
+        exec(statement)
+    except BaseException as error:
+        errors.append(repr(error))
+
+
+threads = [
+    threading.Thread(target=resolve, args=(statements[index % 2],))
+    for index in range(thread_count)
+]
+for thread in threads:
+    thread.start()
+for thread in threads:
+    thread.join(15)
+if errors:
+    print("\n".join(sorted(set(errors))))
+    raise SystemExit(1)
+"""
+        # The lazy names resolve once per process, so each attempt needs a
+        # fresh interpreter.
+        for attempt in range(8):
+            result = subprocess.run(
+                [sys.executable, "-c", script],
+                env=os.environ.copy(),
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                universal_newlines=True,
+                timeout=30,
+            )
+            self.assertEqual(
+                0, result.returncode,
+                "attempt {}: {}".format(attempt, result.stdout),
+            )
+
+    def test_fresh_import_of_cycle_prone_leaf_modules(self):
+        # Each module must be importable as the very first pypaimon import
+        # of a process. Heavy package initializers used to pull circular
+        # chains (index_file_meta -> globalindex -> create_global_index)
+        # that the eager root import happened to mask.
+        for module in [
+            "pypaimon.index.index_file_meta",
+            "pypaimon.manifest.index_manifest_entry",
+            "pypaimon.read.scanner.bucket_select_converter",
+            "pypaimon.table.data_evolution_merge_into",
+            "pypaimon.write.table_delete",
+            "pypaimon.tag.tag_manager",
+        ]:
+            result = subprocess.run(
+                [sys.executable, "-c", "import " + module],
+                env=os.environ.copy(),
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                universal_newlines=True,
+                timeout=60,
+            )
+            self.assertEqual(
+                0, result.returncode,
+                "{}: {}".format(module, result.stdout),
+            )
+
+
+if __name__ == "__main__":
+    unittest.main()

Reply via email to