Skip to content

Commit 8e745a4

Browse files
authored
fix(qdrant): harden rollout provisioning and memberLeave reentry (#3172)
1 parent 558ba28 commit 8e745a4

10 files changed

Lines changed: 1479 additions & 223 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ KubeBlocks add-ons.
3333
| polardbx | polardbx-cdc-2.3.0<br>polardbx-cn-2.3.0<br>polardbx-dn-2.3.0<br>polardbx-gms-2.3.0 | PolarDB-X is a cloud native distributed SQL Database designed for high concurrency, massive storage, complex querying scenarios. | ApeCloud Vettal Wu |
3434
| postgresql | postgresql-12.14.0<br>postgresql-12.14.1<br>postgresql-12.15.0<br>postgresql-12.22.0<br>postgresql-14.18.0<br>postgresql-14.23.0<br>postgresql-14.7.2<br>postgresql-14.8.0<br>postgresql-15.13.0<br>postgresql-15.18.0<br>postgresql-15.7.0<br>postgresql-16.14.0<br>postgresql-16.4.0<br>postgresql-16.9.0<br>postgresql-17.10.0<br>postgresql-17.5.0<br>postgresql-18.1.0<br>postgresql-18.4.0 | A PostgreSQL (with Patroni HA) cluster definition Helm chart for Kubernetes | ldming dingshun-cmss |
3535
| pulsar | pulsar-bookies-recovery-2.11.2<br>pulsar-bookies-recovery-3.0.2<br>pulsar-bookies-recovery-4.0.6<br>pulsar-bookkeeper-2.11.2<br>pulsar-bookkeeper-3.0.2<br>pulsar-bookkeeper-4.0.6<br>pulsar-broker-2.11.2<br>pulsar-broker-3.0.2<br>pulsar-broker-4.0.6<br>pulsar-proxy-2.11.2<br>pulsar-proxy-3.0.2<br>pulsar-proxy-4.0.6<br>pulsar-zookeeper-2.11.2<br>pulsar-zookeeper-3.0.2 | Apache Pulsar is an open-source, distributed messaging and streaming platform built for the cloud. | cjc7373 caiq1nyu |
36-
| qdrant | qdrant-1.10.0<br>qdrant-1.13.4<br>qdrant-1.15.4<br>qdrant-1.16.3<br>qdrant-1.17.1<br>qdrant-1.5.0<br>qdrant-1.7.3<br>qdrant-1.8.1<br>qdrant-1.8.4 | High-performance, massive-scale Vector Database for the next generation of AI. | cjc7373 |
36+
| qdrant | qdrant-1.10.0<br>qdrant-1.13.4<br>qdrant-1.15.4<br>qdrant-1.15.5<br>qdrant-1.16.3<br>qdrant-1.17.1<br>qdrant-1.18.2<br>qdrant-1.5.0<br>qdrant-1.7.3<br>qdrant-1.8.1<br>qdrant-1.8.4 | High-performance, massive-scale Vector Database for the next generation of AI. | cjc7373 |
3737
| rabbitmq | rabbitmq-3.10.25<br>rabbitmq-3.11.28<br>rabbitmq-3.12.14<br>rabbitmq-3.13.7<br>rabbitmq-3.8.34<br>rabbitmq-3.9.29<br>rabbitmq-4.0.9<br>rabbitmq-4.1.6<br>rabbitmq-4.2.1<br>rabbitmq-4.3.1 | RabbitMQ is a reliable and mature messaging and streaming broker. | xuriwuyun |
3838
| redis | redis-5.0.12<br>redis-6.2.14<br>redis-6.2.17<br>redis-6.2.18<br>redis-6.2.19<br>redis-6.2.22<br>redis-7.0.6<br>redis-7.2.10<br>redis-7.2.11<br>redis-7.2.12<br>redis-7.2.14<br>redis-7.2.4<br>redis-7.2.7<br>redis-7.4.2<br>redis-7.4.5<br>redis-7.4.6<br>redis-7.4.7<br>redis-7.4.9<br>redis-8.0.1<br>redis-8.0.3<br>redis-8.0.4<br>redis-8.0.5<br>redis-8.2.1<br>redis-8.2.2<br>redis-8.2.3<br>redis-8.2.6<br>redis-8.4.0<br>redis-8.4.3<br>redis-8.6.3<br>redis-cluster-5.0.12<br>redis-cluster-6.2.14<br>redis-cluster-6.2.17<br>redis-cluster-6.2.18<br>redis-cluster-6.2.19<br>redis-cluster-6.2.22<br>redis-cluster-7.0.6<br>redis-cluster-7.2.10<br>redis-cluster-7.2.11<br>redis-cluster-7.2.12<br>redis-cluster-7.2.14<br>redis-cluster-7.2.4<br>redis-cluster-7.2.7<br>redis-cluster-7.4.2<br>redis-cluster-7.4.5<br>redis-cluster-7.4.6<br>redis-cluster-7.4.7<br>redis-cluster-7.4.9<br>redis-cluster-8.0.1<br>redis-cluster-8.0.3<br>redis-cluster-8.0.4<br>redis-cluster-8.0.5<br>redis-cluster-8.2.1<br>redis-cluster-8.2.2<br>redis-cluster-8.2.3<br>redis-cluster-8.2.6<br>redis-cluster-8.4.0<br>redis-cluster-8.4.3<br>redis-cluster-8.6.3<br>redis-sentinel-5.0.12<br>redis-sentinel-6.2.14<br>redis-sentinel-6.2.17<br>redis-sentinel-6.2.18<br>redis-sentinel-6.2.19<br>redis-sentinel-6.2.22<br>redis-sentinel-7.0.6<br>redis-sentinel-7.2.10<br>redis-sentinel-7.2.11<br>redis-sentinel-7.2.12<br>redis-sentinel-7.2.14<br>redis-sentinel-7.2.4<br>redis-sentinel-7.2.7<br>redis-sentinel-7.4.2<br>redis-sentinel-7.4.5<br>redis-sentinel-7.4.6<br>redis-sentinel-7.4.7<br>redis-sentinel-7.4.9<br>redis-sentinel-8.0.1<br>redis-sentinel-8.0.3<br>redis-sentinel-8.0.4<br>redis-sentinel-8.0.5<br>redis-sentinel-8.2.1<br>redis-sentinel-8.2.2<br>redis-sentinel-8.2.3<br>redis-sentinel-8.2.6<br>redis-sentinel-8.4.0<br>redis-sentinel-8.4.3<br>redis-sentinel-8.6.3<br>redis-twemproxy-0.5.0 | Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, Streams, HyperLogLogs, Bitmaps. | wangyelei |
3939
| risingwave | risingwave-compactor-v1.0.0<br>risingwave-compute-v1.0.0<br>risingwave-connector-v1.0.0<br>risingwave-frontend-v1.0.0<br>risingwave-meta-v1.0.0 | RisingWave is a distributed SQL streaming database that enables cost-efficient and reliable processing of streaming data. | RisingWave Labs |

addons/qdrant/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type: application
88
version: 1.2.0-alpha.0
99

1010
# This is the version number of qdrant.
11-
appVersion: "1.17.1"
11+
appVersion: "1.18.2"
1212

1313
# Add a dependency to the kubeblocks definition library chart
1414
dependencies:
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
# shellcheck shell=bash
2+
# shellcheck disable=SC2034
3+
4+
if ! validate_shell_type_and_version "bash" 4 &>/dev/null; then
5+
echo "qdrant_member_leave_spec.sh skip cases because dependency bash version 4 or higher is not installed."
6+
exit 0
7+
fi
8+
9+
Describe "Qdrant Member Leave Bash Script Tests"
10+
export QDRANT_MEMBER_LEAVE_UNIT_TEST=true
11+
Include ../scripts/qdrant-member-leave.sh
12+
13+
cleanup() {
14+
unset QDRANT_MEMBER_LEAVE_UNIT_TEST KB_LEAVE_MEMBER_POD_NAME
15+
unset qdrant_member_leave_deadline qdrant_member_leave_phase_deadline
16+
}
17+
After "cleanup"
18+
19+
Describe "peer pod URI matching"
20+
cluster_info='{"result":{"peers":{"1":{"uri":"http://qdrant-cluster-qdrant-1.qdrant-cluster-qdrant-headless.default.svc.cluster.local:6335/"},"10":{"uri":"http://qdrant-cluster-qdrant-10.qdrant-cluster-qdrant-headless.default.svc.cluster.local:6335/"}}}}'
21+
22+
It "matches the exact pod host and does not also match ordinal 10"
23+
When call qdrant_peer_id_for_pod "$cluster_info" "qdrant-cluster-qdrant-1"
24+
The status should be success
25+
The output should eq "1"
26+
End
27+
28+
It "keeps ordinal 10 as a surviving control endpoint when ordinal 1 leaves"
29+
KB_LEAVE_MEMBER_POD_NAME="qdrant-cluster-qdrant-1"
30+
When call qdrant_control_uris_from_cluster_info "$cluster_info"
31+
The status should be success
32+
The output should eq "http://qdrant-cluster-qdrant-10.qdrant-cluster-qdrant-headless.default.svc.cluster.local:6333"
33+
End
34+
End
35+
36+
Describe "qdrant_select_target_peer_id_for_shard()"
37+
cluster_info='{"result":{"peers":{"1":{"uri":"http://qdrant-0:6335/"},"2":{"uri":"http://qdrant-1:6335/"},"3":{"uri":"http://qdrant-2:6335/"}}}}'
38+
39+
It "uses the raft leader when it does not already own the shard"
40+
collection_info='{"result":{"peer_id":3,"local_shards":[{"shard_id":8}],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[]}}'
41+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
42+
The status should be success
43+
The output should eq "2"
44+
End
45+
46+
It "skips a leader that owns the shard as a remote replica"
47+
collection_info='{"result":{"peer_id":3,"local_shards":[{"shard_id":8}],"remote_shards":[{"shard_id":7,"peer_id":1},{"shard_id":7,"peer_id":2}],"shard_transfers":[]}}'
48+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
49+
The status should be success
50+
The output should eq "3"
51+
End
52+
53+
It "skips a leader that owns the shard locally"
54+
collection_info='{"result":{"peer_id":2,"local_shards":[{"shard_id":7}],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[]}}'
55+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
56+
The status should be success
57+
The output should eq "3"
58+
End
59+
60+
It "skips an in-flight transfer destination for the shard"
61+
collection_info='{"result":{"peer_id":3,"local_shards":[{"shard_id":8}],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[{"shard_id":7,"from":3,"to":2}]}}'
62+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
63+
The status should be success
64+
The output should eq "3"
65+
End
66+
67+
It "returns empty when every surviving peer already owns the shard"
68+
collection_info='{"result":{"peer_id":2,"local_shards":[{"shard_id":7}],"remote_shards":[{"shard_id":7,"peer_id":1},{"shard_id":7,"peer_id":3}],"shard_transfers":[]}}'
69+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
70+
The status should be success
71+
The output should eq ""
72+
End
73+
74+
It "fails closed when a local shard has no serving peer identity"
75+
collection_info='{"result":{"local_shards":[{"shard_id":7}],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[]}}'
76+
When call qdrant_select_target_peer_id_for_shard "$cluster_info" "$collection_info" "1" "2" "7"
77+
The status should be failure
78+
The stderr should include "local shard without peer_id"
79+
End
80+
End
81+
82+
Describe "qdrant_shard_on_leaving_peer()"
83+
It "does not attribute a surviving peer local shard to the leaving peer"
84+
collection_info='{"result":{"peer_id":2,"local_shards":[{"shard_id":7}],"remote_shards":[],"shard_transfers":[]}}'
85+
When call qdrant_shard_on_leaving_peer "$collection_info" "7" "1"
86+
The status should be failure
87+
End
88+
89+
It "attributes a local shard using the serving peer identity"
90+
collection_info='{"result":{"peer_id":1,"local_shards":[{"shard_id":7}],"remote_shards":[],"shard_transfers":[]}}'
91+
When call qdrant_shard_on_leaving_peer "$collection_info" "7" "1"
92+
The status should be success
93+
End
94+
95+
It "fails closed when a matching local shard has no serving peer identity"
96+
collection_info='{"result":{"local_shards":[{"shard_id":7}],"remote_shards":[],"shard_transfers":[]}}'
97+
When call qdrant_shard_on_leaving_peer "$collection_info" "7" "1"
98+
The status should eq 2
99+
The stderr should include "local shard without peer_id"
100+
End
101+
End
102+
103+
Describe "memberLeave action-wide deadline"
104+
cleanup_deadline() {
105+
unset QDRANT_MEMBER_LEAVE_ACTION_SECONDS QDRANT_MEMBER_LEAVE_CURL_TIMEOUT
106+
unset QDRANT_MEMBER_LEAVE_WAIT_SECONDS QDRANT_MEMBER_LEAVE_FINALIZE_SECONDS
107+
unset qdrant_member_leave_deadline qdrant_member_leave_phase_deadline
108+
}
109+
After "cleanup_deadline"
110+
111+
It "starts one shared deadline before preflight work"
112+
SECONDS=7
113+
QDRANT_MEMBER_LEAVE_ACTION_SECONDS=50
114+
When call qdrant_initialize_member_leave_deadline
115+
The status should be success
116+
The variable qdrant_member_leave_deadline should eq 57
117+
End
118+
119+
It "rejects a script budget that consumes the kbagent safety buffer"
120+
QDRANT_MEMBER_LEAVE_ACTION_SECONDS=51
121+
When call qdrant_initialize_member_leave_deadline
122+
The status should be failure
123+
The stderr should include "between 1 and 50"
124+
End
125+
126+
It "reserves finalization time inside the shared action budget"
127+
SECONDS=10
128+
qdrant_member_leave_deadline=60
129+
QDRANT_MEMBER_LEAVE_WAIT_SECONDS=100
130+
QDRANT_MEMBER_LEAVE_FINALIZE_SECONDS=10
131+
132+
When call qdrant_initialize_member_leave_drain_deadline
133+
The status should be success
134+
The variable qdrant_member_leave_phase_deadline should eq 50
135+
End
136+
137+
It "clips each curl call to the remaining action budget"
138+
SECONDS=10
139+
qdrant_member_leave_deadline=13
140+
QDRANT_MEMBER_LEAVE_CURL_TIMEOUT=5
141+
qdrant_curl() {
142+
printf '%s\n' "$*"
143+
}
144+
145+
When call qdrant_member_leave_curl "http://control/cluster"
146+
The status should be success
147+
The output should eq "-sf --max-time 3 http://control/cluster"
148+
End
149+
150+
It "fails before a curl when the shared action budget is exhausted"
151+
SECONDS=10
152+
qdrant_member_leave_deadline=10
153+
qdrant_curl() {
154+
echo "unexpected qdrant_curl call" >&2
155+
return 99
156+
}
157+
158+
When call qdrant_member_leave_curl "http://control/cluster"
159+
The status should be failure
160+
The stderr should include "memberLeave action budget exhausted"
161+
The stderr should not include "unexpected qdrant_curl call"
162+
End
163+
164+
It "does not reset the deadline after preflight"
165+
qdrant_member_leave_deadline=12345
166+
qdrant_move_shards() {
167+
echo "deadline=${qdrant_member_leave_deadline}"
168+
}
169+
qdrant_remove_peer() {
170+
return 0
171+
}
172+
173+
When call qdrant_leave_member
174+
The status should be success
175+
The output should include "deadline=12345"
176+
End
177+
End
178+
179+
Describe "memberLeave replay safety"
180+
setup_replay_state() {
181+
export JQ="${JQ:-jq}"
182+
control_uri="http://control"
183+
leave_peer_uri="http://leaving"
184+
leave_peer_id="1"
185+
leader_peer_id="2"
186+
cluster_info='{"result":{"peers":{"1":{"uri":"http://qdrant-0:6335/"},"2":{"uri":"http://qdrant-1:6335/"},"3":{"uri":"http://qdrant-2:6335/"}}}}'
187+
qdrant_member_leave_deadline=$((SECONDS + 50))
188+
}
189+
Before "setup_replay_state"
190+
191+
It "does not submit a duplicate move when the shard transfer already exists"
192+
moving_info='{"result":{"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[{"shard_id":7,"from":1,"to":2}]}}'
193+
qdrant_curl() {
194+
echo "unexpected qdrant_curl call" >&2
195+
return 99
196+
}
197+
198+
When call qdrant_submit_shard_move_if_needed "demo" "7" "$moving_info"
199+
The status should be success
200+
The output should include "already moving"
201+
End
202+
203+
It "treats a failed move submit as success when replay sees the transfer in progress"
204+
initial_info='{"result":{"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[]}}'
205+
qdrant_curl() {
206+
last_arg="${*: -1}"
207+
case "$last_arg" in
208+
"http://control/collections/demo/cluster")
209+
if [ "$1" = "-sf" ] && [ "$4" = "-X" ]; then
210+
return 1
211+
fi
212+
echo '{"result":{"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[{"shard_id":7,"from":1,"to":2}]}}'
213+
;;
214+
*)
215+
return 1
216+
;;
217+
esac
218+
}
219+
220+
When call qdrant_submit_shard_move_if_needed "demo" "7" "$initial_info"
221+
The status should be success
222+
The output should include "already moving after failed submit"
223+
End
224+
225+
It "treats a failed move submit as success when replay sees the shard local to a surviving peer"
226+
initial_info='{"result":{"peer_id":2,"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[]}}'
227+
qdrant_curl() {
228+
last_arg="${*: -1}"
229+
case "$last_arg" in
230+
"http://control/collections/demo/cluster")
231+
if [ "$1" = "-sf" ] && [ "$4" = "-X" ]; then
232+
return 1
233+
fi
234+
echo '{"result":{"peer_id":2,"local_shards":[{"shard_id":7}],"remote_shards":[],"shard_transfers":[]}}'
235+
;;
236+
*)
237+
return 1
238+
;;
239+
esac
240+
}
241+
242+
When call qdrant_submit_shard_move_if_needed "demo" "7" "$initial_info"
243+
The status should be success
244+
The output should include "moved off peer 1 after failed submit"
245+
End
246+
247+
It "submits the move to an unused peer instead of an existing replica"
248+
replicated_info='{"result":{"peer_id":3,"local_shards":[{"shard_id":8}],"remote_shards":[{"shard_id":7,"peer_id":1},{"shard_id":7,"peer_id":2}],"shard_transfers":[]}}'
249+
qdrant_curl() {
250+
printf '%s\n' "$*"
251+
}
252+
253+
When call qdrant_submit_shard_move_if_needed "demo" "7" "$replicated_info"
254+
The status should be success
255+
The output should include '"to_peer_id":3'
256+
The output should not include '"to_peer_id":2'
257+
End
258+
259+
It "does not submit a move when every surviving peer already owns the shard"
260+
replicated_info='{"result":{"peer_id":2,"local_shards":[{"shard_id":7}],"remote_shards":[{"shard_id":7,"peer_id":1},{"shard_id":7,"peer_id":3}],"shard_transfers":[]}}'
261+
qdrant_curl() {
262+
echo "unexpected qdrant_curl call" >&2
263+
return 99
264+
}
265+
266+
When call qdrant_submit_shard_move_if_needed "demo" "7" "$replicated_info"
267+
The status should be failure
268+
The stderr should include "refusing to reduce replica count"
269+
The stderr should not include "unexpected qdrant_curl call"
270+
End
271+
272+
It "discovers remaining shards from the control endpoint when the leaving endpoint is unavailable"
273+
qdrant_curl() {
274+
last_arg="${*: -1}"
275+
case "$last_arg" in
276+
"http://leaving/collections/demo/cluster")
277+
return 7
278+
;;
279+
"http://control/collections/demo/cluster")
280+
echo '{"result":{"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1},{"shard_id":8,"peer_id":2}],"shard_transfers":[]}}'
281+
;;
282+
*)
283+
return 1
284+
;;
285+
esac
286+
}
287+
288+
When call qdrant_leaving_shards_for_collection "demo"
289+
The status should be success
290+
The output should include "7"
291+
The stderr should include "leaving peer endpoint unavailable"
292+
End
293+
294+
It "treats peer removal as successful when the peer is absent after a failed delete response"
295+
qdrant_remove_peer_deleted_marker="$(mktemp)"
296+
rm -f "$qdrant_remove_peer_deleted_marker"
297+
qdrant_curl() {
298+
last_arg="${*: -1}"
299+
case "$last_arg" in
300+
"http://control/cluster")
301+
if [ -f "$qdrant_remove_peer_deleted_marker" ]; then
302+
echo '{"result":{"peers":{"2":{"uri":"http://qdrant-1:6335/"}}}}'
303+
else
304+
echo '{"result":{"peers":{"1":{"uri":"http://qdrant-0:6335/"},"2":{"uri":"http://qdrant-1:6335/"}}}}'
305+
fi
306+
;;
307+
"http://control/cluster/peer/1")
308+
touch "$qdrant_remove_peer_deleted_marker"
309+
return 1
310+
;;
311+
*)
312+
return 1
313+
;;
314+
esac
315+
}
316+
317+
When call qdrant_remove_peer
318+
The status should be success
319+
The output should include "absent after failed delete"
320+
End
321+
322+
It "returns failure when shards are not drained within the bounded action window"
323+
export QDRANT_MEMBER_LEAVE_WAIT_SECONDS=0
324+
qdrant_member_leave_deadline="$SECONDS"
325+
qdrant_curl() {
326+
last_arg="${*: -1}"
327+
case "$last_arg" in
328+
"http://control/collections/demo/cluster")
329+
echo '{"result":{"local_shards":[],"remote_shards":[{"shard_id":7,"peer_id":1}],"shard_transfers":[{"shard_id":7,"from":1,"to":2}]}}'
330+
;;
331+
*)
332+
return 1
333+
;;
334+
esac
335+
}
336+
337+
When call qdrant_wait_for_collection_drained "demo"
338+
The status should be failure
339+
The output should include "timed out waiting"
340+
End
341+
End
342+
End

0 commit comments

Comments
 (0)