1use std::collections::BTreeMap;
12
13use krabka_protocol::owned::delete_groups_request::DeleteGroupsRequest;
14
15use crate::{
16 AdminClient, KafkaError,
17 group_coordinators::{GroupOutcome, group_error_outcome},
18 group_descriptions::request_error_outcomes,
19};
20
21pub type GroupDeletions = BTreeMap<String, Result<(), KafkaError>>;
24
25impl AdminClient {
26 pub async fn delete_consumer_groups(&self, groups: &[&str]) -> GroupDeletions {
44 self.delete_groups("deleteConsumerGroups", groups).await
45 }
46
47 pub async fn delete_share_groups(&self, groups: &[&str]) -> GroupDeletions {
54 self.delete_groups("deleteShareGroups", groups).await
55 }
56
57 pub async fn delete_streams_groups(&self, groups: &[&str]) -> GroupDeletions {
63 self.delete_consumer_groups(groups).await
64 }
65
66 async fn delete_groups(&self, api: &'static str, groups: &[&str]) -> GroupDeletions {
68 self.call_group_coordinators(api, groups, |coordinator, keys| async move {
69 let connection = self.connect_coordinator(&coordinator).await?;
70 let request = DeleteGroupsRequest {
71 groups_names: keys.clone(),
72 ..Default::default()
73 };
74 Ok(match connection.send(request).await {
75 Ok(response) => response
76 .results
77 .into_iter()
78 .map(|result| {
79 let outcome = match result.error_code {
80 0 => GroupOutcome::Done(Ok(())),
81 code => group_error_outcome(code, None, &[]),
82 };
83 (result.group_id, outcome)
84 })
85 .collect(),
86 Err(error) => request_error_outcomes(&keys, &error.into()),
87 })
88 })
89 .await
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use std::{
96 net::SocketAddr,
97 sync::{Arc, Mutex},
98 };
99
100 use assert2::assert;
101 use krabka_client_core::MockReply;
102 use krabka_protocol::owned::{
103 delete_groups_request,
104 delete_groups_response::{DeletableGroupResult, DeleteGroupsResponse},
105 find_coordinator_request,
106 };
107
108 use super::*;
109 use crate::{
110 group_coordinators::{
111 kafka_error,
112 test_support::{GroupCluster, Lookup, group_cluster},
113 },
114 partition_leaders::test_support::{ApiVersionRange, decode_request, encode_response},
115 };
116
117 fn names(names: &[&str]) -> Vec<String> {
118 names.iter().map(|name| (*name).to_owned()).collect()
119 }
120
121 async fn delete_groups_cluster(find_coordinator_max: i16) -> GroupCluster {
122 group_cluster()
123 .count(1)
124 .apis(vec![
125 ApiVersionRange::builder()
126 .api_key(delete_groups_request::API_KEY)
127 .max_version(2)
128 .build(),
129 ApiVersionRange::builder()
130 .api_key(find_coordinator_request::API_KEY)
131 .max_version(find_coordinator_max)
132 .build(),
133 ])
134 .lookup(|_, _| Lookup::At(0))
135 .answer(mock_delete_groups)
136 .call()
137 .await
138 }
139
140 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
145 async fn delete_groups_batches_and_retries_as_kafka_does() {
146 #[derive(Clone, Copy, Debug)]
147 enum Call {
148 Consumer,
149 Share,
150 Streams,
151 }
152 for call in [Call::Consumer, Call::Share, Call::Streams] {
153 let codes = Arc::new(Mutex::new(maplit::btreemap! {
155 "done" => vec![0],
156 "loading" => vec![14, 0],
157 "moved" => vec![16, 0],
158 "busy" => vec![68],
159 }));
160 let cluster = group_cluster()
161 .count(2)
162 .apis(vec![
163 ApiVersionRange::builder()
164 .api_key(delete_groups_request::API_KEY)
165 .max_version(2)
166 .build(),
167 ])
168 .lookup(|group, lookups| match (group, lookups) {
169 ("denied", _) => Lookup::Error(30),
170 ("moved", 0) => Lookup::At(1),
171 _ => Lookup::At(0),
172 })
173 .answer(move |_, api_key, version, body, _| {
174 if api_key != delete_groups_request::API_KEY {
175 return MockReply::Silent;
176 }
177 let request: DeleteGroupsRequest = decode_request(body, version, version >= 2);
178 let mut codes = codes.lock().expect("codes lock");
179 let results = request
180 .groups_names
181 .iter()
182 .map(|group| {
183 let script = codes.get_mut(group.as_str()).expect("scripted group");
184 let error_code = if script.len() > 1 {
185 script.remove(0)
186 } else {
187 script[0]
188 };
189 DeletableGroupResult {
190 group_id: group.clone(),
191 error_code,
192 ..Default::default()
193 }
194 })
195 .collect();
196 MockReply::Respond(encode_response(
197 &DeleteGroupsResponse {
198 results,
199 ..Default::default()
200 },
201 version,
202 version >= 2,
203 ))
204 })
205 .call()
206 .await;
207 let admin = cluster.admin(krabka_units::secs(5)).await;
208 let groups = ["done", "loading", "moved", "busy", "denied"];
209
210 let result = match call {
211 Call::Consumer => admin.delete_consumer_groups(&groups).await,
212 Call::Share => admin.delete_share_groups(&groups).await,
213 Call::Streams => admin.delete_streams_groups(&groups).await,
214 };
215
216 let seen = cluster.stop();
217 let requests = seen
218 .requests
219 .iter()
220 .map(|request| {
221 let body: DeleteGroupsRequest =
222 decode_request(&request.body, request.version, request.version >= 2);
223 (request.broker, request.version, body.groups_names)
224 })
225 .collect::<Vec<_>>();
226 let lookups = seen
227 .find_coordinator
228 .into_iter()
229 .map(|(_, keys)| keys)
230 .collect::<Vec<_>>();
231 assert!(
232 (result, lookups)
233 == (
234 maplit::btreemap! {
235 "busy".to_owned() => Err(kafka_error(68, None)),
236 "denied".to_owned() => Err(kafka_error(30, None)),
237 "done".to_owned() => Ok(()),
238 "loading".to_owned() => Ok(()),
239 "moved".to_owned() => Ok(()),
240 },
241 vec![
242 names(&["busy", "denied", "done", "loading", "moved"]),
243 names(&["moved"]),
244 ],
245 ),
246 "case {call:?}"
247 );
248 let mut first_round = requests[..2].to_vec();
251 first_round.sort();
252 assert!(
253 (first_round, requests[2..].to_vec())
254 == (
255 vec![
256 (0, 2, names(&["busy", "done", "loading"])),
257 (1, 2, names(&["moved"])),
258 ],
259 vec![(0, 2, names(&["loading", "moved"]))],
260 ),
261 "case {call:?}"
262 );
263 }
264 }
265
266 fn mock_delete_groups(
267 _broker: usize,
268 api_key: i16,
269 version: i16,
270 body: &[u8],
271 _addr: SocketAddr,
272 ) -> MockReply {
273 if api_key != delete_groups_request::API_KEY {
274 return MockReply::Silent;
275 }
276 let request: DeleteGroupsRequest = decode_request(body, version, version >= 2);
277 MockReply::Respond(encode_response(
278 &DeleteGroupsResponse {
279 results: request
280 .groups_names
281 .into_iter()
282 .map(|group_id| DeletableGroupResult {
283 group_id,
284 ..Default::default()
285 })
286 .collect(),
287 ..Default::default()
288 },
289 version,
290 version >= 2,
291 ))
292 }
293
294 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
299 async fn group_lookups_batch_from_find_coordinator_v4_and_fan_out_below() {
300 for (name, find_coordinator_max, expected) in [
301 ("v6 broker batches", 6, vec![(4, names(&["a", "b", "c"]))]),
302 ("v4 broker batches", 4, vec![(4, names(&["a", "b", "c"]))]),
303 (
304 "v3 broker looks up each group",
305 3,
306 vec![(3, names(&["a"])), (3, names(&["b"])), (3, names(&["c"]))],
307 ),
308 ] {
309 let cluster = delete_groups_cluster(find_coordinator_max).await;
310 let admin = cluster.admin(krabka_units::secs(5)).await;
311
312 let result = admin.delete_consumer_groups(&["a", "b", "c"]).await;
313
314 let mut lookups = cluster
315 .stop()
316 .find_coordinator
317 .into_iter()
318 .map(|(version, keys)| (version.min(4), keys))
319 .collect::<Vec<_>>();
320 lookups.sort();
321 assert!(
322 (result, lookups)
323 == (
324 maplit::btreemap! {
325 "a".to_owned() => Ok(()),
326 "b".to_owned() => Ok(()),
327 "c".to_owned() => Ok(()),
328 },
329 expected,
330 ),
331 "case {name}"
332 );
333 }
334 }
335
336 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
341 async fn delete_groups_looks_up_one_group_at_a_time_below_find_coordinator_v4() {
342 let cluster = delete_groups_cluster(3).await;
343 let admin = cluster.admin(krabka_units::secs(5)).await;
344
345 let result = admin.delete_consumer_groups(&["a", "b"]).await;
346
347 let seen = cluster.stop();
348 assert!(
349 (result, seen.find_coordinator, seen.requests.len())
350 == (
351 maplit::btreemap! {
352 "a".to_owned() => Ok(()),
353 "b".to_owned() => Ok(()),
354 },
355 vec![(3, names(&["a"])), (3, names(&["b"]))],
356 1,
357 )
358 );
359 }
360}