mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-06 10:00:58 -05:00
* Reset first_voter and prune address book on first-peer --reinit
A peer removed from consensus and killed after `RemoveNode(self)` was
committed but before it was applied keeps the old cluster's
`first_voter` and peer addresses in `raft_state.json`. First-peer
`--reinit` reset `conf_state` to a single voter (itself) but left both
untouched (`first_voter` is in fact never reset, even when the removal
is applied).
Both values are served to peers bootstrapping onto the reinitialized
cluster. A joining peer seeds its initial `conf_state` with the
advertised `first_voter`, and Raft conf-changes are deltas on top of
that base - so a stale `first_voter` permanently corrupts the joining
peer's voter set: it ends up with {old first peer, itself}, missing the
actual leader. Its `/readyz` then treats the still-alive old peer as a
cluster member and waits for the old cluster's commit index, which its
own consensus never reaches.
This only manifests when the reinitialized leader replicates its log as
plain entries (nothing applied before the kill, so the log anchor is
index 0). If the leader sends a snapshot instead, the snapshot's full
`conf_state` heals the corrupted seed - which is why
test_reinit_removed_peer only failed sporadically on CI.
Fix first-peer `--reinit` to behave like founding a fresh cluster:
reset `first_voter` to this peer (not `None`, or `recover_first_voter`
would re-derive the old first voter from the retained Raft log) and
prune `peer_address_by_id` to this peer only.
The kill-before-apply state is now injected deterministically into
test_reinit_removed_peer, reproducing the exact CI failure against the
unfixed binary. Since `--reinit` now prunes the address book, the
stale-address injection in
test_reinit_removed_peer_readyz_ignores_old_cluster moved to a restart
without `--reinit`, so it keeps exercising the /readyz `conf_state`
membership filter from #9688.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix test_reinit_consensus expecting stale address book after --reinit
The test waited for cluster size 2 right after starting the reinitialized
first peer, before the second peer was even started. That only passed
because first-peer --reinit used to keep the old cluster's addresses in
the address book - the stale state the previous commit removes. A
reinitialized first peer is a fresh single-member cluster; the other
peers re-join and re-register their new addresses right after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
import pathlib
|
|
from operator import itemgetter
|
|
from random import randrange
|
|
from time import sleep
|
|
|
|
from .fixtures import create_collection, drop_collection
|
|
from .utils import *
|
|
|
|
N_PEERS = 2
|
|
N_SHARDS = 2
|
|
N_REPLICAS = 1
|
|
N_COLLECTION_LOOPS = 3
|
|
COLLECTION_NAME = "test_collection"
|
|
POINTS_JSON = {
|
|
"points": [
|
|
{"id": 1, "vector": [0.05, 0.61, 0.76, 0.74], "payload": {"city": "Berlin"}},
|
|
{"id": 2, "vector": [0.19, 0.81, 0.75, 0.11], "payload": {"city": "London"}},
|
|
{"id": 3, "vector": [0.36, 0.55, 0.47, 0.94], "payload": {"city": "Paris"}},
|
|
{"id": 4, "vector": [0.18, 0.01, 0.85, 0.80], "payload": {"city": "Malaga"}},
|
|
{"id": 5, "vector": [0.24, 0.18, 0.22, 0.44], "payload": {"city": "New York"}},
|
|
{"id": 6, "vector": [0.35, 0.08, 0.11, 0.44], "payload": {"city": "Munich"}},
|
|
{"id": 7, "vector": [0.45, 0.07, 0.21, 0.04], "payload": {"city": "Madrid"}},
|
|
{"id": 8, "vector": [0.75, 0.18, 0.91, 0.48], "payload": {"city": "Prague"}},
|
|
{"id": 9, "vector": [0.30, 0.01, 0.10, 0.12], "payload": {"city": "Bratislava"}},
|
|
{"id": 10, "vector": [0.95, 0.8, 0.17, 0.19], "payload": {"city": "Tokyo"}},
|
|
]
|
|
}
|
|
|
|
|
|
def get_collection_points(uri):
|
|
req_json = {
|
|
"limit": len(POINTS_JSON["points"]),
|
|
"with_payload": True,
|
|
"with_vector": True
|
|
}
|
|
res = requests.post(
|
|
f"{uri}/collections/{COLLECTION_NAME}/points/scroll", json=req_json
|
|
)
|
|
assert_http_ok(res)
|
|
return res.json()["result"]["points"]
|
|
|
|
|
|
def compare_points(uri):
|
|
original_points = POINTS_JSON["points"]
|
|
fetched_points = get_collection_points(uri)
|
|
original_points, fetched_points = [sorted(l, key=itemgetter('id')) for l in (original_points, fetched_points)]
|
|
pairs = zip(original_points, fetched_points)
|
|
diff = [(x, y) for x, y in pairs if x != y]
|
|
assert len(diff) == 0, f'Original and final points are not equal, diff: "{diff}"'
|
|
|
|
|
|
def test_reinit_consensus(tmp_path: pathlib.Path):
|
|
assert_project_root()
|
|
peer_urls, peer_dirs, bootstrap_url = start_cluster(tmp_path, N_PEERS)
|
|
|
|
create_collection(peer_urls[0], shard_number=N_SHARDS, replication_factor=N_REPLICAS)
|
|
wait_collection_exists_and_active_on_all_peers(collection_name=COLLECTION_NAME, peer_api_uris=peer_urls)
|
|
|
|
for i in range(N_COLLECTION_LOOPS):
|
|
drop_collection(peer_urls[randrange(N_PEERS)], collection=COLLECTION_NAME)
|
|
|
|
create_collection(peer_urls[randrange(N_PEERS)], shard_number=N_SHARDS, replication_factor=N_REPLICAS)
|
|
wait_collection_exists_and_active_on_all_peers(collection_name=COLLECTION_NAME, peer_api_uris=peer_urls)
|
|
|
|
r_batch = requests.put(
|
|
f"{peer_urls[randrange(N_PEERS)]}/collections/{COLLECTION_NAME}/points?wait=true", json=POINTS_JSON)
|
|
assert_http_ok(r_batch)
|
|
|
|
# assert data in both shards
|
|
for peer_api_uri in peer_urls:
|
|
info = get_collection_cluster_info(peer_api_uri, COLLECTION_NAME)
|
|
for shard in info["local_shards"]:
|
|
assert shard["points_count"] > 0
|
|
|
|
print("Stop the peers, keep data")
|
|
for _ in range(N_PEERS):
|
|
processes.pop().kill()
|
|
|
|
print("Start the peers with new urls")
|
|
peer_urls_new = []
|
|
(bootstrap_api_uri, bootstrap_uri) = start_first_peer(peer_dirs[0], "peer_0_restarted.log", reinit=True)
|
|
peer_urls_new.append(bootstrap_api_uri)
|
|
# `--reinit` founds a fresh single-member cluster: the old peers (and their
|
|
# old addresses) are dropped from the address book, the other peers re-join
|
|
# below and re-register their new addresses.
|
|
leader = wait_peer_added(bootstrap_api_uri, expected_size=1)
|
|
for i in range(1, len(peer_dirs)):
|
|
peer_urls_new.append(start_peer(peer_dirs[i], f"peer_{i}_restarted.log", bootstrap_uri, reinit=True))
|
|
wait_for_uniform_cluster_status(peer_urls_new, leader)
|
|
wait_collection_exists_and_active_on_all_peers(collection_name=COLLECTION_NAME, peer_api_uris=peer_urls_new)
|
|
|
|
compare_points(peer_urls_new[0])
|
|
|
|
print("Add one more peer")
|
|
peer_dir_additional = make_peer_folder(tmp_path, N_PEERS)
|
|
peer_dirs.append(peer_dir_additional)
|
|
peer_url_additional = start_peer(peer_dir_additional, f"peer_{N_PEERS}_additional.log", bootstrap_uri)
|
|
wait_peer_added(bootstrap_api_uri, expected_size=3)
|
|
peer_urls_new.append(peer_url_additional)
|
|
wait_for_uniform_cluster_status(peer_urls_new, leader)
|
|
wait_collection_exists_and_active_on_all_peers(collection_name=COLLECTION_NAME, peer_api_uris=peer_urls_new)
|
|
compare_points(peer_urls_new[1])
|