mirror of
https://github.com/hanzoai/vector.git
synced 2026-10-30 18:18:56 +00:00
394 lines
12 KiB
Python
394 lines
12 KiB
Python
import multiprocessing
|
|
import pathlib
|
|
|
|
from .custom_sharding import create_collection_with_custom_sharding, create_shard
|
|
from .fixtures import *
|
|
from .utils import *
|
|
|
|
COLLECTION_NAME = "tenant_collection"
|
|
|
|
N_PEERS = 3
|
|
N_SHARDS = 1
|
|
N_REPLICAS = 1
|
|
|
|
|
|
def match_results(results1: List[dict], results2: List[dict]):
|
|
# Compare ids and payloads of search results
|
|
if len(results1) != len(results2):
|
|
return False, f"Results length differ: {len(results1)} != {len(results2)}"
|
|
|
|
for idx, (r1, r2) in enumerate(zip(results1, results2)):
|
|
if r1['id'] != r2['id']:
|
|
return False, f"Result IDs differ: {r1['id']} != {r2['id']} at index {idx}"
|
|
if r1.get('payload') != r2.get('payload'):
|
|
return False, f"Result payloads differ for ID {r1['id']}: {r1.get('payload')} != {r2.get('payload')}, at index {idx}"
|
|
|
|
return True, ""
|
|
|
|
|
|
def scroll_points(peer_url, shard_key, limit=10):
|
|
resp = requests.post(
|
|
f"{peer_url}/collections/{COLLECTION_NAME}/points/scroll",
|
|
json={
|
|
"limit": limit,
|
|
"with_payload": True,
|
|
"with_vector": False,
|
|
"shard_key": shard_key,
|
|
}
|
|
)
|
|
assert_http_ok(resp)
|
|
|
|
results = resp.json()["result"]["points"]
|
|
return results
|
|
|
|
def search_points(peer_url):
|
|
query_vector = [0.1, 0.2, 0.3, 0.4]
|
|
resp = requests.post(
|
|
f"{peer_url}/collections/{COLLECTION_NAME}/points/search",
|
|
json={
|
|
"vector": query_vector,
|
|
"top": 5,
|
|
"filter": {
|
|
"must": [
|
|
{
|
|
"key": "tenant",
|
|
"match": {
|
|
"value": "tenant1"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"shard_key": {
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
}
|
|
}
|
|
)
|
|
assert_http_ok(resp)
|
|
|
|
results = resp.json()["result"]
|
|
return results
|
|
|
|
|
|
def test_tenant_promotion_simple(tmp_path: pathlib.Path):
|
|
assert_project_root()
|
|
|
|
peer_api_uris, peer_dirs, bootstrap_uri = start_cluster(tmp_path, N_PEERS)
|
|
|
|
create_collection_with_custom_sharding(peer_api_uris[0], collection=COLLECTION_NAME, shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS)
|
|
|
|
create_shard(
|
|
peer_url=peer_api_uris[0],
|
|
collection=COLLECTION_NAME,
|
|
shard_key="default",
|
|
shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS,
|
|
# initial_state="Partial",
|
|
)
|
|
|
|
n_points = 1_000
|
|
upsert_random_points(
|
|
peer_api_uris[0],
|
|
n_points,
|
|
collection_name=COLLECTION_NAME,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
},
|
|
extra_payload={
|
|
"tenant": "tenant1"
|
|
}
|
|
)
|
|
|
|
n_points = 100
|
|
upsert_random_points(
|
|
peer_api_uris[0],
|
|
n_points,
|
|
collection_name=COLLECTION_NAME,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant2"
|
|
},
|
|
extra_payload={
|
|
"tenant": "tenant2"
|
|
}
|
|
)
|
|
|
|
# Search point with shard key and filter
|
|
results = search_points(peer_api_uris[0])
|
|
assert len(results) > 0, "No results found for tenant1"
|
|
|
|
create_shard(
|
|
peer_url=peer_api_uris[0],
|
|
collection=COLLECTION_NAME,
|
|
shard_key="tenant1",
|
|
shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS,
|
|
initial_state="Partial",
|
|
)
|
|
|
|
# With new shard we should get the same results
|
|
result2 = search_points(peer_api_uris[0])
|
|
match, msg = match_results(results, result2)
|
|
assert match, f"Results differ after tenant1 shard creation: {msg}"
|
|
|
|
# Check that global read requests work
|
|
_scroll = scroll_points(peer_api_uris[0], shard_key=None, limit=10)
|
|
|
|
# Check that collection info is still available
|
|
_info = get_collection_info(peer_api_uris[0], COLLECTION_NAME)
|
|
|
|
n_points = 100
|
|
upsert_random_points(
|
|
peer_api_uris[0],
|
|
n_points,
|
|
collection_name=COLLECTION_NAME,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
},
|
|
extra_payload={
|
|
"tenant": "tenant1"
|
|
}
|
|
)
|
|
|
|
results = search_points(peer_api_uris[0])
|
|
|
|
# Promote to dedicated shard
|
|
resp = requests.post(
|
|
f"{peer_api_uris[0]}/collections/{COLLECTION_NAME}/cluster",
|
|
json={
|
|
"replicate_points": {
|
|
"filter": {
|
|
"must": {
|
|
"key": "tenant",
|
|
"match": {
|
|
"value": "tenant1"
|
|
}
|
|
}
|
|
},
|
|
"from_shard_key": "default",
|
|
"to_shard_key": "tenant1"
|
|
}
|
|
}
|
|
)
|
|
assert_http_ok(resp)
|
|
|
|
results2 = search_points(peer_api_uris[0])
|
|
match, msg = match_results(results, results2)
|
|
assert match, f"Results differ after tenant1 promotion start: {msg}"
|
|
|
|
# Wait all transfers complete on all peers
|
|
for peer_url in peer_api_uris:
|
|
wait_for_collection_shard_transfers_count(peer_url, COLLECTION_NAME, 0)
|
|
# Check that new shard have active state
|
|
info = get_collection_cluster_info(peer_api_uris[0], COLLECTION_NAME)
|
|
for shard in info["local_shards"]:
|
|
assert shard['state'] == 'Active'
|
|
for shard in info["remote_shards"]:
|
|
assert shard['state'] == 'Active'
|
|
|
|
results3 = search_points(peer_api_uris[0])
|
|
match, msg = match_results(results, results3)
|
|
assert match, f"Results differ after tenant1 promotion complete: {msg}"
|
|
|
|
# Delete records from the old shard
|
|
resp = requests.post(
|
|
f"{peer_api_uris[0]}/collections/{COLLECTION_NAME}/points/delete?wait=true",
|
|
json={
|
|
"filter": {
|
|
"must": {
|
|
"key": "tenant",
|
|
"match": {
|
|
"value": "tenant1"
|
|
}
|
|
}
|
|
},
|
|
"shard_key": "default"
|
|
}
|
|
)
|
|
assert_http_ok(resp)
|
|
|
|
results4 = search_points(peer_api_uris[0])
|
|
match, msg = match_results(results, results4)
|
|
assert match, f"Results differ after tenant1 deletion from old shard: {msg}"
|
|
|
|
|
|
def update_points_in_loop(peer_urls: List[str], collection_name, offset=0, duration=None):
|
|
start = time.time()
|
|
limit = 3
|
|
|
|
while True:
|
|
peer_url = random.choice(peer_urls)
|
|
upsert_random_points(
|
|
peer_url,
|
|
limit,
|
|
collection_name,
|
|
offset=offset,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
},
|
|
extra_payload={
|
|
"tenant": "tenant1"
|
|
}
|
|
)
|
|
offset += limit
|
|
|
|
if duration is not None and (time.time() - start) > duration:
|
|
break
|
|
|
|
|
|
def run_update_points_in_background(peer_urls, collection_name, init_offset=0, duration=None):
|
|
p = multiprocessing.Process(target=update_points_in_loop, args=(peer_urls, collection_name, init_offset, duration))
|
|
p.start()
|
|
return p
|
|
|
|
|
|
def set_payload_in_loop(peer_urls: List[str], collection_name, from_id, to_id, duration=None):
|
|
start = time.time()
|
|
|
|
while True:
|
|
peer_url = random.choice(peer_urls)
|
|
point_id = random.randint(from_id, to_id - 1)
|
|
|
|
update_points_payload(
|
|
peer_url,
|
|
points=[point_id],
|
|
collection_name=collection_name,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
},
|
|
)
|
|
|
|
if duration is not None and (time.time() - start) > duration:
|
|
break
|
|
|
|
def run_set_payload_in_background(peer_urls, collection_name, from_id, to_id, duration=None):
|
|
p = multiprocessing.Process(target=set_payload_in_loop, args=(peer_urls, collection_name, from_id, to_id, duration))
|
|
p.start()
|
|
return p
|
|
|
|
def test_tenant_promotion_background_updates(tmp_path: pathlib.Path):
|
|
assert_project_root()
|
|
|
|
peer_api_uris, peer_dirs, bootstrap_uri = start_cluster(tmp_path, N_PEERS)
|
|
|
|
create_collection_with_custom_sharding(peer_api_uris[0], collection=COLLECTION_NAME, shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS)
|
|
|
|
create_shard(
|
|
peer_url=peer_api_uris[0],
|
|
collection=COLLECTION_NAME,
|
|
shard_key="default",
|
|
shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS,
|
|
)
|
|
|
|
n_points = 1_000
|
|
upsert_random_points(
|
|
peer_api_uris[0],
|
|
n_points,
|
|
collection_name=COLLECTION_NAME,
|
|
shard_key={
|
|
"fallback": "default",
|
|
"target": "tenant1"
|
|
},
|
|
extra_payload={
|
|
"tenant": "tenant1"
|
|
}
|
|
)
|
|
|
|
# Create tenant1 shard in Partial state
|
|
create_shard(
|
|
peer_url=peer_api_uris[0],
|
|
collection=COLLECTION_NAME,
|
|
shard_key="tenant1",
|
|
shard_number=N_SHARDS,
|
|
replication_factor=N_REPLICAS,
|
|
initial_state="Partial",
|
|
)
|
|
|
|
# Start background process with updates
|
|
upload_process_1 = run_update_points_in_background(peer_api_uris, COLLECTION_NAME, init_offset=10000, duration=5)
|
|
set_payload_process_1 = run_set_payload_in_background(peer_api_uris, COLLECTION_NAME, from_id=0, to_id=1000, duration=5)
|
|
|
|
time.sleep(1) # Let some updates happen
|
|
|
|
# Promote to dedicated shard
|
|
resp = requests.post(
|
|
f"{peer_api_uris[0]}/collections/{COLLECTION_NAME}/cluster",
|
|
json={
|
|
"replicate_points": {
|
|
"filter": {
|
|
"must": {
|
|
"key": "tenant",
|
|
"match": {
|
|
"value": "tenant1"
|
|
}
|
|
}
|
|
},
|
|
"from_shard_key": "default",
|
|
"to_shard_key": "tenant1"
|
|
}
|
|
}
|
|
)
|
|
assert_http_ok(resp)
|
|
|
|
# Wait all transfers complete on all peers
|
|
for peer_url in peer_api_uris:
|
|
wait_for_collection_shard_transfers_count(peer_url, COLLECTION_NAME, 0)
|
|
# Check that new shard have active state
|
|
info = get_collection_cluster_info(peer_api_uris[0], COLLECTION_NAME)
|
|
for shard in info["local_shards"]:
|
|
assert shard['state'] == 'Active'
|
|
for shard in info["remote_shards"]:
|
|
assert shard['state'] == 'Active'
|
|
|
|
# Stop background updates and check that `tenant1` contains all points (full sequence)
|
|
|
|
upload_process_1.kill()
|
|
set_payload_process_1.kill()
|
|
|
|
# Read all points from offset 10000 and check that there is no gaps
|
|
# Use scroll API to get all points
|
|
|
|
response = requests.post(
|
|
f"{peer_api_uris[0]}/collections/{COLLECTION_NAME}/points/scroll",
|
|
json={
|
|
"limit": 1000000,
|
|
"with_payload": False,
|
|
"with_vector": False,
|
|
"shard_key": "tenant1",
|
|
"offset": 10000,
|
|
"filter": {
|
|
"must": {
|
|
"key": "tenant",
|
|
"match": {
|
|
"value": "tenant1"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
assert_http_ok(response)
|
|
points = response.json()["result"]["points"]
|
|
|
|
received_ids = set()
|
|
for point in points:
|
|
received_ids.add(point['id'])
|
|
|
|
missing_points = []
|
|
# Check that there is no gaps in IDs from 10000 to max received ID
|
|
if points:
|
|
max_id = max(received_ids)
|
|
print("Max ID in tenant1 shard:", max_id)
|
|
for expected_id in range(10000, max_id + 1):
|
|
if expected_id not in received_ids:
|
|
missing_points.append(expected_id)
|
|
|
|
assert len(missing_points) == 0, f"Missing point ID {missing_points} in tenant1 shard"
|