| # 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) |
| """ |
| # Lazy names resolve once per process, so retries need fresh ones. |
| 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), |
| ) |
| |
| @unittest.skipIf( |
| sys.version_info[:2] < (3, 7), |
| "module-level lazy attributes require Python 3.7+", |
| ) |
| def test_concurrent_leaf_imports_and_top_level_export(self): |
| # Racing leaf imports against top-level exports deadlocks whenever a |
| # package init eagerly imports siblings that import the package back. |
| script = r""" |
| import sys |
| import threading |
| |
| sys.setswitchinterval(1e-6) |
| statements = [ |
| "import pypaimon.manifest.schema.data_file_meta", |
| "import pypaimon.manifest.schema.simple_stats", |
| "from pypaimon import Schema", |
| "from pypaimon import CatalogFactory", |
| "import pypaimon.multimodal.connection", |
| "import pypaimon.read.query_auth_split", |
| "import pypaimon.write.table_delete", |
| "import pypaimon.index.index_file_meta", |
| ] |
| barrier = threading.Barrier(len(statements)) |
| errors = [] |
| |
| |
| def resolve(statement): |
| try: |
| barrier.wait() |
| exec(statement) |
| except BaseException as error: |
| errors.append(repr(error)) |
| |
| |
| threads = [ |
| threading.Thread(target=resolve, args=(statement,)) |
| for statement in statements |
| ] |
| for thread in threads: |
| thread.start() |
| for thread in threads: |
| thread.join(15) |
| if errors: |
| print("\n".join(sorted(set(errors)))) |
| raise SystemExit(1) |
| """ |
| 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 import cleanly as a process's first pypaimon |
| # import, without a package init pulling a circular chain. |
| 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() |