blob: 12cedaf1ec8e16396cbef3a5a06f743601bb7db5 [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.
"""Exercise Ray vector-index construction with real worker processes."""
import os
import threading
import time
from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import patch
import pytest
ray = pytest.importorskip("ray")
pytest.importorskip("paimon_vindex")
from pypaimon.globalindex.create_global_index import GlobalIndexBuilder
from pypaimon.index.index_file_handler import IndexFileHandler
from pypaimon.execution.python.globalindex.create_global_index import PythonGlobalIndexBuilder
from pypaimon.tests import ray_vector_search_test as fixtures
from pypaimon.tests.ray_vector_search_test import add_rows, VECTORS
ray_cluster = fixtures.ray_cluster
table = fixtures.table
OPTIONS = {"global-index.row-count-per-shard": "3", "ivf-flat.nlist": "1"}
def index_files(table):
return [entry.index_file for entry in IndexFileHandler(table.raw_table).scan(
table.raw_table.snapshot_manager().get_latest_snapshot())
if entry.index_file.index_type == "ivf-flat"]
@pytest.mark.parametrize("concurrency", [1, 2])
def test_build_and_incremental_coverage(table, ray_cluster, concurrency):
add_rows(table, VECTORS)
table.delete("id = 0")
before = table.raw_table.snapshot_manager().get_latest_snapshot().id
assert table.create_index("embedding", "ivf-flat", options=OPTIONS,
execution="ray", concurrency=concurrency) == 2
assert table.raw_table.snapshot_manager().get_latest_snapshot().id == before + 1
files = sorted(index_files(table), key=lambda f: f.global_index_meta.row_range_start)
assert [(f.global_index_meta.row_range_start, f.global_index_meta.row_range_end) for f in files] == [(0, 2), (3, 5)]
query = table.search([1., 1.], column="embedding", options={"ivf.nprobe": "1"}).select(["id"]).limit(2)
assert query.to_list() == query.to_list(execution="ray")
assert all(row["id"] != 0 for row in query.to_list())
assert table.create_index("embedding", "ivf-flat", options=OPTIONS, execution="ray") == 0
add_rows(table, [[20., 1.]], 6)
assert table.create_index("embedding", "ivf-flat", options=OPTIONS, execution="ray") == 1
assert len(index_files(table)) == 3
def test_build_messages_do_not_publish_or_include_later_appends(table, ray_cluster):
add_rows(table, VECTORS[:3])
builder = GlobalIndexBuilder(table.raw_table, "embedding", "ivf-flat", options=OPTIONS)
snapshot = table.raw_table.snapshot_manager().get_latest_snapshot().id
messages = builder.build(execution="ray")
assert table.raw_table.snapshot_manager().get_latest_snapshot().id == snapshot
assert not index_files(table)
add_rows(table, VECTORS[3:], 3)
commit = table.raw_table.new_batch_write_builder().new_commit()
try:
commit.commit(messages)
finally:
commit.close()
assert [(f.global_index_meta.row_range_start, f.global_index_meta.row_range_end)
for f in index_files(table)] == [(0, 2)]
assert table.create_index("embedding", "ivf-flat", options=OPTIONS, execution="ray") == 1
def _write_then_fail(context, shard, name):
if shard[1].from_:
time.sleep(0.3)
from pypaimon.execution.python.globalindex.ray_index_build import _build_shard
message = _build_shard(context, shard, name)
if not shard[1].from_:
raise RuntimeError("failed after writing an index file")
return message
def test_worker_failure_drains_and_cleans_all_outputs(table, ray_cluster):
add_rows(table, VECTORS)
before = table.raw_table.snapshot_manager().get_latest_snapshot().id
# Pass a real serialized worker function; a failure after finish() must also
# clean files whose commit messages were never delivered to the driver.
original_remote = ray.remote
def remote(worker):
return original_remote(_write_then_fail)
with patch.object(ray, "remote", remote):
with pytest.raises(Exception, match="failed after writing"):
table.create_index("embedding", "ivf-flat", options=OPTIONS,
execution="ray", concurrency=2)
assert table.raw_table.snapshot_manager().get_latest_snapshot().id == before
assert not index_files(table)
root = table.raw_table.path_factory().global_index_path_factory().global_index_root_path()
from pathlib import Path
assert not list(Path(root).glob("*.index"))
assert table.create_index("embedding", "ivf-flat", options=OPTIONS, execution="ray") == 2
@pytest.mark.parametrize("kwargs,match", [
({"execution": "other"}, "execution"),
({"concurrency": 1}, "execution='ray'"),
({"execution": "ray", "concurrency": 0}, "concurrency"),
({"execution": "ray", "ray_remote_args": {"max_retries": 1}}, "max_retries"),
({"execution": "ray", "ray_remote_args": {"retry_exceptions": True}}, "max_retries"),
])
def test_validate_before_empty_plan(table, kwargs, match):
with pytest.raises(ValueError, match=match):
table.create_index("embedding", "ivf-flat", options=OPTIONS, **kwargs)
def test_reject_non_vector_ray_build(table):
with pytest.raises(ValueError, match="only native vector"):
table.create_index("id", "btree", execution="ray")
@contextmanager
def _jindo_sdk_stub():
from pypaimon.filesystem import jindo_file_system_handler as jindo_module
from pypaimon.filesystem import pyarrow_file_io as arrow_module
from pypaimon.tests.jindo_file_system_test import _RecordingConfig
def connect(root, user, config):
return SimpleNamespace(pid=os.getpid(), lock=threading.Lock(), config=config.values)
with patch.object(jindo_module, "JINDO_AVAILABLE", True), \
patch.object(arrow_module, "JINDO_AVAILABLE", True), \
patch.object(jindo_module, "jfs", SimpleNamespace(connect=connect)), \
patch.object(jindo_module, "jutil", SimpleNamespace(Config=_RecordingConfig)):
yield
def _inspect_jindo_context(context_refs):
# Nested object refs postpone deserialization until the worker has installed
# the SDK stand-in. Only Config/connect are replaced, not serialization.
with _jindo_sdk_stub():
builder, field, table_read, index_path = ray.get(context_refs[0])
assert isinstance(builder, PythonGlobalIndexBuilder)
handler = builder._table.file_io.filesystem.handler
assert table_read.table.file_io is builder._table.file_io
assert field.name == "embedding"
assert handler._known_file_size("oss://bucket/object") is None
handler.register_file_size("object", 456)
return (os.getpid(), handler._jindo_fs.pid, handler._known_file_size("oss://bucket/object"),
handler.root_path, handler._jindo_fs.config["fs.oss.securityToken"], index_path)
def test_jindo_build_context_reconstructs_on_worker(table, ray_cluster):
from pypaimon.common.options import Options
from pypaimon.filesystem.oss_file_io import OssFileIO
from pypaimon.read.table_read import TableRead
from pypaimon.table.special_fields import SpecialFields
builder = GlobalIndexBuilder(table.raw_table, "embedding", "ivf-flat", options=OPTIONS)
field = table.raw_table.field_dict["embedding"]
reader = TableRead(table.raw_table, None, [field, SpecialFields.ROW_ID])
with _jindo_sdk_stub():
file_io = OssFileIO("oss://bucket/table", Options({
"fs.oss.impl": "jindo", "fs.oss.endpoint": "oss-cn-hangzhou.aliyuncs.com",
"fs.oss.accessKeyId": "test-ak", "fs.oss.accessKeySecret": "test-sk",
"fs.oss.securityToken": "test-token",
}))
original = file_io.filesystem.handler
original.register_file_size("object", 123)
with patch.object(table.raw_table, "file_io", file_io):
# This is the exact tuple broadcast by build_vector_index.
context = ray.put((PythonGlobalIndexBuilder(builder), field, reader, "oss://bucket/index"))
result = ray.get(ray.remote(_inspect_jindo_context).remote([context]))
worker_pid, client_pid, size, root, token, index_path = result
assert worker_pid != os.getpid()
assert client_pid == worker_pid
assert (size, root, token, index_path) == (456, "oss://bucket/", "test-token", "oss://bucket/index")
assert original._known_file_size("oss://bucket/object") == 123