blob: 7ad77220f29febc6c4e9fdfb5f4ae888f68dea97 [file]
# 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.
"""Positional input callbacks and reader input ownership for vector indexes."""
import os
import threading
from concurrent.futures import ThreadPoolExecutor, wait
from urllib.parse import urlparse
from pypaimon.common.file_io import pread, supports_pread
from pypaimon.globalindex.vindex_options import READ_PARALLELISM_PARAMETER
class PaimonVindexInput:
"""Input adapter required by paimon_vindex.VectorIndexReader."""
def __init__(self, stream, parallelism=1):
if isinstance(parallelism, bool) or not isinstance(parallelism, int) or parallelism < 1:
raise ValueError("Vector index read parallelism must be a positive integer")
self._stream = stream
self._parallelism = parallelism
self._read_slots = threading.BoundedSemaphore(parallelism)
self._supports_pread = supports_pread(stream)
self._lock = threading.Lock()
self._executor = None
self._closed = False
def pread_many(self, ranges):
ranges = list(ranges)
with self._lock:
if self._closed:
raise ValueError("Vector index input is closed")
if not self._supports_pread:
chunks = []
for offset, length in ranges:
self._stream.seek(offset)
chunks.append(self._stream.read(length))
return chunks
if self._parallelism > 1 and len(ranges) > 1:
if self._executor is None:
self._executor = ThreadPoolExecutor(
max_workers=self._parallelism,
thread_name_prefix="paimon-vindex-io")
executor = self._executor
if self._parallelism == 1:
with self._read_slots:
return [pread(self._stream, length, offset) for offset, length in ranges]
if executor is None or len(ranges) <= 1:
return [self._pread(offset, length) for offset, length in ranges]
futures = []
try:
for offset, length in ranges:
futures.append(executor.submit(self._pread, offset, length))
return [future.result() for future in futures]
finally:
# A failed range must not leave reads using a stream the caller may close.
wait(futures)
def _pread(self, offset, length):
# Native query workers may also issue single-range callbacks concurrently.
with self._read_slots:
return pread(self._stream, length, offset)
def close(self):
"""Release workers; the owner remains responsible for closing the stream."""
with self._lock:
self._closed = True
executor = self._executor
self._executor = None
if executor is not None:
executor.shutdown(wait=True)
class VindexReaderResources:
"""Manage an index stream and its callbacks without choosing a query engine."""
def __init__(self, file_io, index_path, io_metas, options=None):
assert len(io_metas) == 1, "Expected exactly one index file per shard"
self._file_io = file_io
self._index_path = index_path
self._io_meta = io_metas[0]
self._options = dict(options or {})
self._stream = None
self._index_input = None
self._reader = None
self._metadata = None
self._search_params_type = None
self._load_lock = threading.Lock()
def _ensure_loaded(self):
if self._reader is not None:
return
with self._load_lock:
if self._reader is not None:
return
file_path = (self._io_meta.external_path
if self._io_meta.external_path
else os.path.join(self._index_path, self._io_meta.file_name))
parallelism = _read_parallelism(self._options, file_path)
stream = self._file_io.new_input_stream(file_path)
reader = None
index_input = None
try:
index_input = PaimonVindexInput(stream, parallelism)
reader = self._create_reader(index_input, file_path)
self._initialize_reader(reader)
self._index_input = index_input
self._reader = reader
self._stream = stream
except BaseException:
try:
if reader is not None:
reader.close()
finally:
try:
if index_input is not None:
index_input.close()
finally:
stream.close()
raise
def _create_reader(self, index_input, file_path):
raise NotImplementedError
def _initialize_reader(self, reader):
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
def close(self):
try:
if self._reader is not None:
self._reader.close()
finally:
self._reader = None
try:
if self._index_input is not None:
self._index_input.close()
finally:
self._index_input = None
if self._stream is not None:
self._stream.close()
self._stream = None
def _read_parallelism(options, file_path):
value = options.get(READ_PARALLELISM_PARAMETER)
if value is None:
# Avoid thread scheduling overhead for local files, including Windows paths.
scheme = urlparse(file_path).scheme
return 1 if scheme in ("", "file") or len(scheme) == 1 else 4
try:
parallelism = int(str(value))
except (ValueError, TypeError):
parallelism = 0
if parallelism < 1:
raise ValueError("'%s' must be a positive integer, got: %s"
% (READ_PARALLELISM_PARAMETER, value))
return parallelism
def _filter_bytes(include_row_ids):
if include_row_ids is None:
return None
return include_row_ids.serialize()