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 39b8f86284 [python] Close vector readers when search raises 
synchronously (#9755)
39b8f86284 is described below

commit 39b8f8628429d964af4b3cc31864dc87012d5938
Author: chaoyang <[email protected]>
AuthorDate: Sat Sep 12 21:40:50 2026 +0800

    [python] Close vector readers when search raises synchronously (#9755)
---
 .../pypaimon/table/source/vector_search_read.py    | 12 ++++-
 .../pypaimon/tests/vector_search_filter_test.py    | 55 ++++++++++++++++++++++
 2 files changed, 65 insertions(+), 2 deletions(-)

diff --git a/paimon-python/pypaimon/table/source/vector_search_read.py 
b/paimon-python/pypaimon/table/source/vector_search_read.py
index 46c45f01f3..fe9d965367 100644
--- a/paimon-python/pypaimon/table/source/vector_search_read.py
+++ b/paimon-python/pypaimon/table/source/vector_search_read.py
@@ -255,7 +255,11 @@ class AbstractVectorSearchReadImpl:
 
         reader, offset_reader = self._open_offset_reader(
             vector_index_files, row_range_start, row_range_end)
-        future = offset_reader.visit_vector_search(vector_search)
+        try:
+            future = offset_reader.visit_vector_search(vector_search)
+        except BaseException:
+            reader.close()
+            raise
         future.add_done_callback(lambda _: reader.close())
         return future
 
@@ -397,7 +401,11 @@ class AbstractVectorSearchReadImpl:
 
         reader, offset_reader = self._open_offset_reader(
             vector_index_files, row_range_start, row_range_end)
-        future = offset_reader.visit_batch_vector_search(batch_vector_search)
+        try:
+            future = 
offset_reader.visit_batch_vector_search(batch_vector_search)
+        except BaseException:
+            reader.close()
+            raise
         future.add_done_callback(lambda _: reader.close())
         return future
 
diff --git a/paimon-python/pypaimon/tests/vector_search_filter_test.py 
b/paimon-python/pypaimon/tests/vector_search_filter_test.py
index e3bbccc911..4d0c39e29e 100644
--- a/paimon-python/pypaimon/tests/vector_search_filter_test.py
+++ b/paimon-python/pypaimon/tests/vector_search_filter_test.py
@@ -3566,5 +3566,60 @@ class BatchVectorSearchTest(unittest.TestCase):
         mock.patch.stopall()
 
 
+class VectorSearchReaderCleanupTest(unittest.TestCase):
+
+    def test_reader_lifetime_for_single_and_batch_search(self):
+        from concurrent.futures import Future
+
+        from pypaimon.globalindex.offset_global_index_reader import 
OffsetGlobalIndexReader
+        from pypaimon.table.source.vector_search_read import 
DataEvolutionVectorRead
+
+        column = _field(1, "embedding", "FLOAT")
+        read = DataEvolutionVectorRead(_StubTable([column], []), 1, column, 
[1.0])
+        for batch in (False, True):
+            for outcome in ("sync_error", "pending_success", "pending_error", 
"completed"):
+                with self.subTest(batch=batch, outcome=outcome):
+                    stream = io.BytesIO(b"index")
+                    reader = mock.Mock()
+                    reader.close.side_effect = stream.close
+                    visit = (reader.visit_batch_vector_search if batch
+                             else reader.visit_vector_search)
+                    error = ValueError("Query vector dimension mismatch")
+                    source = Future()
+                    result = [None, None] if batch else None
+                    if outcome == "sync_error":
+                        visit.side_effect = error
+                    else:
+                        visit.return_value = source
+                        if outcome == "completed":
+                            source.set_result(result)
+                    offset = OffsetGlobalIndexReader(reader, 0, 10)
+                    evaluate = read._eval_batch if batch else read._eval
+                    query = [[1.0], [2.0]] if batch else [1.0]
+                    with mock.patch.object(read, "_open_offset_reader",
+                                           return_value=(reader, offset)):
+                        if outcome == "sync_error":
+                            with self.assertRaises(ValueError) as raised:
+                                evaluate(0, 10, [object()], query, 1, None)
+                            self.assertIs(error, raised.exception)
+                        else:
+                            future = evaluate(0, 10, [object()], query, 1, 
None)
+                            if outcome.startswith("pending"):
+                                reader.close.assert_not_called()
+                                self.assertFalse(stream.closed)
+                                if outcome == "pending_error":
+                                    source.set_exception(error)
+                                else:
+                                    source.set_result(result)
+                            if outcome == "pending_error":
+                                with self.assertRaises(ValueError) as raised:
+                                    future.result()
+                                self.assertIs(error, raised.exception)
+                            else:
+                                self.assertEqual(result, future.result())
+                    reader.close.assert_called_once_with()
+                    self.assertTrue(stream.closed)
+
+
 if __name__ == "__main__":
     unittest.main()

Reply via email to