1use std::collections::{HashMap, HashSet};
8
9use krabka_units::convert::TimeExt as _;
10
11use crate::{
12 commit::OffsetAndMetadata, consumer::Consumer, coordinator::CoordinatorRetryPolicy,
13 error::ConsumerError, offset_wire, poll::is_reset_sentinel,
14};
15
16pub(crate) type PausedPartitions = std::sync::Mutex<HashMap<(String, i32), u64>>;
25
26impl Consumer {
27 pub async fn pause(&self, partitions: &[(String, i32)]) -> Result<(), ConsumerError> {
39 let ownership_ids = self.commit_identity.lock().await.ownership_ids.clone();
40 let mut paused = Vec::with_capacity(partitions.len());
41 for partition in partitions {
42 let Some(ownership_id) = ownership_ids.get(partition) else {
43 return Err(no_current_assignment(partition));
44 };
45 paused.push((partition.clone(), *ownership_id));
46 }
47 self.paused
48 .lock()
49 .unwrap_or_else(std::sync::PoisonError::into_inner)
50 .extend(paused);
51 Ok(())
52 }
53
54 pub async fn resume(&self, partitions: &[(String, i32)]) -> Result<(), ConsumerError> {
61 let ownership_ids = self.commit_identity.lock().await.ownership_ids.clone();
62 if let Some(partition) = partitions
63 .iter()
64 .find(|partition| !ownership_ids.contains_key(*partition))
65 {
66 return Err(no_current_assignment(partition));
67 }
68 let mut paused = self
69 .paused
70 .lock()
71 .unwrap_or_else(std::sync::PoisonError::into_inner);
72 for partition in partitions {
73 paused.remove(partition);
74 }
75 Ok(())
76 }
77
78 pub async fn paused(&self) -> HashSet<(String, i32)> {
81 let ownership_ids = self.commit_identity.lock().await.ownership_ids.clone();
82 self.paused_of(&ownership_ids)
83 }
84
85 pub(crate) fn paused_of(
87 &self,
88 ownership_ids: &HashMap<(String, i32), u64>,
89 ) -> HashSet<(String, i32)> {
90 let mut paused = self
91 .paused
92 .lock()
93 .unwrap_or_else(std::sync::PoisonError::into_inner);
94 paused.retain(|partition, ownership_id| ownership_ids.get(partition) == Some(ownership_id));
95 paused.keys().cloned().collect()
96 }
97
98 pub async fn position(
111 &self,
112 topic: impl Into<String>,
113 partition: i32,
114 ) -> Result<i64, ConsumerError> {
115 let key = (topic.into(), partition);
116 let deadline = tokio::time::Instant::now() + self.default_api_timeout.to_std();
117 let wait = async {
118 loop {
119 if !self.assigned.lock().await.contains(&key) {
120 return Err(no_current_assignment(&key));
121 }
122 if let Some(offset) = self.valid_position(&key).await {
123 return Ok(offset);
124 }
125 self.update_fetch_positions(None, Some(deadline)).await?;
126 if self.valid_position(&key).await.is_none() {
127 tokio::time::sleep(self.retry_policy.initial_backoff).await;
128 }
129 }
130 };
131 tokio::time::timeout_at(deadline, wait)
134 .await
135 .unwrap_or_else(|_| {
136 Err(ConsumerError::timeout(format!(
137 "the position for partition {}-{} could not be determined",
138 key.0, key.1
139 )))
140 })
141 }
142
143 async fn valid_position(&self, key: &(String, i32)) -> Option<i64> {
147 let offsets = self.next_offsets.lock().await;
148 let positions = self.positions.lock().await;
149 let offset = *offsets.get(key)?;
150 let awaiting_validation = positions
151 .get(key)
152 .is_some_and(|position| position.awaiting_validation);
153 (!is_reset_sentinel(offset) && !awaiting_validation).then_some(offset)
154 }
155
156 pub async fn committed(
167 &self,
168 partitions: &[(String, i32)],
169 ) -> Result<HashMap<(String, i32), Option<OffsetAndMetadata>>, ConsumerError> {
170 let deadline = tokio::time::Instant::now() + self.default_api_timeout.to_std();
171 self.committed_until(partitions, deadline).await
172 }
173
174 pub(crate) async fn committed_until(
176 &self,
177 partitions: &[(String, i32)],
178 deadline: tokio::time::Instant,
179 ) -> Result<HashMap<(String, i32), Option<OffsetAndMetadata>>, ConsumerError> {
180 self.require_group_id()?;
181 if partitions.is_empty() {
182 return Ok(HashMap::new());
183 }
184 let mut by_topic: HashMap<String, Vec<i32>> = HashMap::new();
185 for (topic, partition) in partitions {
186 by_topic.entry(topic.clone()).or_default().push(*partition);
187 }
188 let request = offset_wire::build_offset_fetch(&self.group_id, &by_topic);
189 let fetch = crate::coordinator::send_offset_fetch(
190 &self.client,
191 &self.group_id,
192 &self.coordinator_id,
193 &request,
194 CoordinatorRetryPolicy {
195 timeout: deadline.saturating_duration_since(tokio::time::Instant::now()),
196 ..self.retry_policy
197 },
198 );
199 let response = tokio::time::timeout_at(deadline, fetch)
202 .await
203 .map_err(|_| {
204 ConsumerError::timeout(
205 "the last committed offsets could not be determined".to_owned(),
206 )
207 })??;
208 let mut committed = offset_wire::parse_committed_offsets(&response);
209 committed.retain(|partition, _| partitions.contains(partition));
210 for partition in partitions {
211 committed.entry(partition.clone()).or_insert(None);
212 }
213 Ok(committed)
214 }
215}
216
217fn no_current_assignment((topic, partition): &(String, i32)) -> ConsumerError {
218 ConsumerError::NoCurrentAssignment {
219 topic: topic.clone(),
220 partition: *partition,
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use std::collections::HashSet;
227
228 use krabka_client_core::{Client, MockBroker};
229 use krabka_protocol::{
230 Encode,
231 owned::{
232 api_versions_request,
233 api_versions_response::{ApiVersion, ApiVersionsResponse},
234 offset_fetch_request,
235 offset_fetch_response::{
236 OffsetFetchResponse, OffsetFetchResponsePartition, OffsetFetchResponseTopic,
237 },
238 },
239 };
240 use krabka_units::secs;
241
242 use crate::{commit::OffsetAndMetadata, poll::partition_error_tests::consumer_with_client};
243
244 fn encode(response: &impl Encode, version: i16) -> Vec<u8> {
245 let mut buf = bytes::BytesMut::new();
246 response.encode(&mut buf, version).expect("encode");
247 buf.to_vec()
248 }
249
250 async fn offset_fetch_coordinator(rows: Vec<(i32, i64, i32, &'static str, i16)>) -> MockBroker {
254 MockBroker::start(move |api_key, version, _corr_id, _body| {
255 if api_key == api_versions_request::API_KEY {
256 let versions = ApiVersionsResponse {
257 api_keys: [
258 (api_versions_request::API_KEY, 0, 3),
259 (offset_fetch_request::API_KEY, 5, 5),
260 ]
261 .into_iter()
262 .map(|(api_key, min_version, max_version)| ApiVersion {
263 api_key,
264 min_version,
265 max_version,
266 ..Default::default()
267 })
268 .collect(),
269 ..Default::default()
270 };
271 return Some(encode(&versions, 0));
272 }
273 if api_key != offset_fetch_request::API_KEY {
274 return None;
275 }
276 let response = OffsetFetchResponse {
277 topics: vec![OffsetFetchResponseTopic {
278 name: "orders".into(),
279 partitions: rows
280 .iter()
281 .map(
282 |(
283 partition_index,
284 committed_offset,
285 committed_leader_epoch,
286 metadata,
287 error_code,
288 )| {
289 OffsetFetchResponsePartition {
290 partition_index: *partition_index,
291 committed_offset: *committed_offset,
292 committed_leader_epoch: *committed_leader_epoch,
293 metadata: Some((*metadata).into()),
294 error_code: *error_code,
295 ..Default::default()
296 }
297 },
298 )
299 .collect(),
300 ..Default::default()
301 }],
302 ..Default::default()
303 };
304 Some(encode(&response, version))
305 })
306 .await
307 }
308
309 #[tokio::test]
313 async fn committed_returns_kafkas_offsets_and_metadata() {
314 let mock = offset_fetch_coordinator(vec![
315 (0, 10, 3, "note", 0),
316 (1, -1, -1, "", 0),
317 (3, 7, -1, "", 0),
318 ])
319 .await;
320 let client = Client::builder()
321 .bootstrap(mock.addr.to_string())
322 .request_timeout(secs(30))
323 .build()
324 .await
325 .expect("client");
326 let consumer = consumer_with_client(client);
327 let partitions = [
328 ("orders".to_string(), 0),
329 ("orders".to_string(), 1),
330 ("orders".to_string(), 2),
331 ];
332 let committed = consumer
333 .committed(&partitions)
334 .await
335 .map_err(|error| error.to_string());
336 mock.stop();
337 assert2::assert!(
338 committed
339 == Ok(
340 maplit::hashmap! {("orders".to_string(), 0) => Some(OffsetAndMetadata {
341 offset: 10,
342 leader_epoch: Some(3),
343 metadata: "note".into(),
344 }), ("orders".to_string(), 1) => None, ("orders".to_string(), 2) => None}
345 )
346 );
347 }
348
349 #[tokio::test]
353 async fn pause_follows_the_ownership_of_the_partition() {
354 let key = ("orders".to_string(), 0);
355 let mut actual = Vec::new();
356 let mut wanted = Vec::new();
357 for (name, pause, resume, new_ownership, expected_error, expected_paused) in [
358 (
359 "pause",
360 vec![key.clone()],
361 vec![],
362 false,
363 None,
364 vec![key.clone()],
365 ),
366 (
367 "pause and resume",
368 vec![key.clone()],
369 vec![key.clone()],
370 false,
371 None,
372 vec![],
373 ),
374 ("owned again", vec![key.clone()], vec![], true, None, vec![]),
375 (
376 "unowned partition",
377 vec![("orders".to_string(), 9)],
378 vec![],
379 false,
380 Some("no current assignment for partition orders-9"),
381 vec![],
382 ),
383 ] {
384 let mock = offset_fetch_coordinator(vec![]).await;
385 let client = Client::builder()
386 .bootstrap(mock.addr.to_string())
387 .build()
388 .await
389 .expect("client");
390 let consumer = consumer_with_client(client);
391 let mut error = consumer
392 .pause(&pause)
393 .await
394 .err()
395 .map(|error| error.to_string());
396 if let Err(resume_error) = consumer.resume(&resume).await {
397 error.get_or_insert(resume_error.to_string());
398 }
399 if new_ownership {
400 consumer
401 .commit_identity
402 .lock()
403 .await
404 .ownership_ids
405 .insert(key.clone(), 2);
406 }
407 let paused = consumer.paused().await;
408 mock.stop();
409 actual.push((name, error, paused));
410 wanted.push((
411 name,
412 expected_error.map(str::to_owned),
413 expected_paused.into_iter().collect::<HashSet<_>>(),
414 ));
415 }
416 assert2::assert!(actual == wanted);
417 }
418
419 #[tokio::test]
422 async fn committed_returns_by_the_api_timeout_when_the_coordinator_is_silent() {
423 let mock = MockBroker::start(|api_key, _version, _corr_id, _body| {
424 (api_key == api_versions_request::API_KEY).then(|| {
425 encode(
426 &ApiVersionsResponse {
427 api_keys: [
428 (api_versions_request::API_KEY, 0, 3),
429 (offset_fetch_request::API_KEY, 5, 5),
430 ]
431 .into_iter()
432 .map(|(api_key, min_version, max_version)| ApiVersion {
433 api_key,
434 min_version,
435 max_version,
436 ..Default::default()
437 })
438 .collect(),
439 ..Default::default()
440 },
441 0,
442 )
443 })
444 })
445 .await;
446 let client = Client::builder()
447 .bootstrap(mock.addr.to_string())
448 .request_timeout(secs(30))
449 .build()
450 .await
451 .expect("client");
452 let mut consumer = consumer_with_client(client);
453 consumer.default_api_timeout = krabka_units::millis(200);
454 let started = tokio::time::Instant::now();
455 let committed = consumer
456 .committed(&[("orders".to_string(), 0)])
457 .await
458 .map_err(|error| error.to_string());
459 let in_time = started.elapsed() < std::time::Duration::from_secs(5);
460 mock.stop();
461 assert2::assert!(
462 (committed, in_time)
463 == (
464 Err("timeout: the last committed offsets could not be determined".to_owned()),
465 true
466 )
467 );
468 }
469
470 #[tokio::test]
473 async fn seek_to_end_keeps_the_end_offset_and_leaves_the_log_end() {
474 let mock = offset_fetch_coordinator(vec![]).await;
475 let client = Client::builder()
476 .bootstrap(mock.addr.to_string())
477 .build()
478 .await
479 .expect("client");
480 let consumer = consumer_with_client(client);
481 let key = ("orders".to_string(), 0);
482 consumer.next_offsets.lock().await.insert(key.clone(), 12);
483 consumer.end_offsets.lock().await.insert(key.clone(), 12);
484 let before = consumer.at_log_end().await;
485 consumer.seek_to_end(&[]).await.expect("seek to end");
486 let after = consumer.at_log_end().await;
487 let end = consumer.end_offsets.lock().await.get(&key).copied();
488 mock.stop();
489 assert2::assert!((before, after, end) == (true, false, Some(12)));
490 }
491
492 #[tokio::test]
497 async fn assign_starts_new_partitions_at_the_committed_offset_or_the_reset() {
498 let mock = offset_fetch_coordinator(vec![(1, 10, 2, "", 0), (2, -1, -1, "", 0)]).await;
499 let client = Client::builder()
500 .bootstrap(mock.addr.to_string())
501 .request_timeout(secs(30))
502 .build()
503 .await
504 .expect("client");
505 let mut consumer = consumer_with_client(client);
506 consumer.subscription = crate::subscription::shared(Vec::new(), None, true);
507 consumer.auto_offset_reset = crate::AutoOffsetReset::Earliest;
508 let key = |partition: i32| ("orders".to_string(), partition);
509 consumer
510 .assign(&[key(0), key(1), key(2)])
511 .await
512 .expect("assign");
513 consumer
516 .positions
517 .lock()
518 .await
519 .entry(key(1))
520 .or_default()
521 .leader_epoch = krabka_ids::LeaderEpoch(5);
522 consumer
523 .resolve_committed_sentinels(None)
524 .await
525 .expect("resolve");
526 let mut offsets: Vec<_> = consumer
527 .next_offsets
528 .lock()
529 .await
530 .clone()
531 .into_iter()
532 .collect();
533 offsets.sort();
534 let identity = consumer.commit_identity.lock().await.clone();
535 let awaiting_validation = consumer
536 .positions
537 .lock()
538 .await
539 .get(&key(1))
540 .is_some_and(|position| position.awaiting_validation);
541 let rejected = consumer
542 .subscribe(["orders"])
543 .await
544 .map_err(|error| error.to_string());
545 mock.stop();
546 assert2::assert!(
547 (
548 offsets,
549 awaiting_validation,
550 identity.generation,
551 identity.member_id,
552 consumer.assignment().await,
553 rejected
554 ) == (
555 vec![(key(0), 5), (key(1), 10), (key(2), crate::poll::BEGINNING_SENTINEL)],
556 true,
557 -1,
558 String::new(),
559 vec![key(0), key(1), key(2)],
560 Err("illegal state: the consumer was built without a subscription, so it has no group membership; build it with subscribe or subscribe_pattern to join a group".to_owned())
561 )
562 );
563 }
564}