1use std::collections::{BTreeMap, BTreeSet, HashMap};
10
11use krabka_client_core::MetadataScope;
12use krabka_protocol::owned::{
13 list_offsets_request,
14 list_offsets_request::{ListOffsetsPartition, ListOffsetsRequest, ListOffsetsTopic},
15 list_offsets_response::ListOffsetsResponse,
16 metadata_request::MetadataRequest,
17 metadata_response::{MetadataResponse, MetadataResponseTopic},
18};
19use krabka_units::convert::TimeExt as _;
20use tokio::time::{Instant, sleep_until, timeout_at};
21
22use crate::{
23 IsolationLevel, consumer::Consumer, coordinator::next_backoff, error::ConsumerError,
24 offset_wire::is_retriable_error, poll, poll::is_reset_sentinel,
25};
26
27const TOPIC_AUTHORIZATION_FAILED: i16 = 29;
29const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
31const INVALID_TOPIC_EXCEPTION: i16 = 17;
33const UNSUPPORTED_FOR_MESSAGE_FORMAT: i16 = 43;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct OffsetAndTimestamp {
39 pub offset: i64,
41 pub timestamp: i64,
43 pub leader_epoch: Option<i32>,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct Node {
50 pub id: i32,
51 pub host: String,
53 pub port: i32,
55 pub rack: Option<String>,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct PartitionInfo {
61 pub topic: String,
62 pub partition: i32,
63 pub leader: Option<Node>,
65 pub replicas: Vec<Node>,
66 pub in_sync_replicas: Vec<Node>,
67 pub offline_replicas: Vec<Node>,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
74struct TimestampListOffsets(ListOffsetsRequest);
75
76impl krabka_protocol::Encode for TimestampListOffsets {
77 fn encode<B: bytes::BufMut>(
78 &self,
79 buf: &mut B,
80 version: i16,
81 ) -> Result<(), krabka_protocol::ProtocolError> {
82 self.0.encode(buf, version)
83 }
84
85 fn encoded_len(&self, version: i16) -> usize {
86 self.0.encoded_len(version)
87 }
88}
89
90impl krabka_protocol::ProtocolRequest for TimestampListOffsets {
91 const API_KEY: i16 = list_offsets_request::API_KEY;
92 const MIN_VERSION: i16 = 1;
93 const MAX_VERSION: i16 = list_offsets_request::MAX_VERSION;
94 const LATEST_STABLE_VERSION: i16 = list_offsets_request::LATEST_STABLE_VERSION;
95 const FLEXIBLE_MIN: i16 = list_offsets_request::FLEXIBLE_MIN;
96 type Response = ListOffsetsResponse;
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101enum QueryRow {
102 Found(OffsetAndTimestamp),
103 NotFound,
106 Unauthorized,
107 Retry,
108}
109
110fn classify_query_row(error_code: i16, offset: i64, timestamp: i64, leader_epoch: i32) -> QueryRow {
112 match error_code {
113 0 if offset != -1 => QueryRow::Found(OffsetAndTimestamp {
114 offset,
115 timestamp,
116 leader_epoch: (leader_epoch >= 0).then_some(leader_epoch),
117 }),
118 0 | UNSUPPORTED_FOR_MESSAGE_FORMAT => QueryRow::NotFound,
119 TOPIC_AUTHORIZATION_FAILED => QueryRow::Unauthorized,
120 _ => QueryRow::Retry,
121 }
122}
123
124fn requested_topic_errors(
130 metadata: &MetadataResponse,
131 topics: &BTreeSet<String>,
132) -> Result<(), ConsumerError> {
133 let requested = |topic: &&MetadataResponseTopic| {
134 topic
135 .name
136 .as_ref()
137 .is_some_and(|name| topics.contains(name))
138 };
139 let unauthorized: BTreeSet<String> = metadata
140 .topics
141 .iter()
142 .filter(requested)
143 .filter(|topic| topic.error_code == TOPIC_AUTHORIZATION_FAILED)
144 .filter_map(|topic| topic.name.clone())
145 .collect();
146 if !unauthorized.is_empty() {
147 return Err(ConsumerError::TopicAuthorizationFailed(unauthorized));
148 }
149 if let Some(name) = metadata
150 .topics
151 .iter()
152 .filter(requested)
153 .find(|topic| topic.error_code == INVALID_TOPIC_EXCEPTION)
154 .and_then(|topic| topic.name.clone())
155 {
156 return Err(ConsumerError::InvalidTopic(name));
157 }
158 Ok(())
159}
160
161fn partition_leaders(metadata: &MetadataResponse) -> HashMap<(String, i32), (i32, i32)> {
163 metadata
164 .topics
165 .iter()
166 .filter(|topic| topic.error_code == 0)
167 .filter_map(|topic| topic.name.as_ref().map(|name| (name, topic)))
168 .flat_map(|(name, topic)| {
169 topic
170 .partitions
171 .iter()
172 .filter(|partition| partition.leader_id >= 0)
173 .map(move |partition| {
174 (
175 (name.clone(), partition.partition_index),
176 (partition.leader_id, partition.leader_epoch),
177 )
178 })
179 })
180 .collect()
181}
182
183type QueryPartition = ((String, i32), i32, i64);
186
187fn query_request(
189 partitions: &[QueryPartition],
190 isolation_level: IsolationLevel,
191) -> ListOffsetsRequest {
192 let mut by_topic: BTreeMap<&str, Vec<ListOffsetsPartition>> = BTreeMap::new();
193 for ((topic, partition), leader_epoch, timestamp) in partitions {
194 by_topic
195 .entry(topic)
196 .or_default()
197 .push(ListOffsetsPartition {
198 partition_index: *partition,
199 current_leader_epoch: *leader_epoch,
200 timestamp: *timestamp,
201 ..Default::default()
202 });
203 }
204 ListOffsetsRequest {
205 replica_id: -1,
206 isolation_level: isolation_level.wire(),
207 topics: by_topic
208 .into_iter()
209 .map(|(name, partitions)| ListOffsetsTopic {
210 name: name.to_owned(),
211 partitions,
212 ..Default::default()
213 })
214 .collect(),
215 ..Default::default()
216 }
217}
218
219fn topic_partition_infos(
223 metadata: &MetadataResponse,
224) -> Result<Option<HashMap<String, Vec<PartitionInfo>>>, ConsumerError> {
225 let unauthorized: BTreeSet<String> = metadata
226 .topics
227 .iter()
228 .filter(|topic| topic.error_code == TOPIC_AUTHORIZATION_FAILED)
229 .filter_map(|topic| topic.name.clone())
230 .collect();
231 if !unauthorized.is_empty() {
232 return Err(ConsumerError::TopicAuthorizationFailed(unauthorized));
233 }
234 let mut retry = false;
235 for topic in &metadata.topics {
236 let name = topic.name.clone().unwrap_or_default();
237 match topic.error_code {
238 0 | UNKNOWN_TOPIC_OR_PARTITION => {}
239 INVALID_TOPIC_EXCEPTION => return Err(ConsumerError::InvalidTopic(name)),
240 code if is_retriable_error(code) => retry = true,
241 code => return Err(ConsumerError::Server(code)),
242 }
243 }
244 if retry {
245 return Ok(None);
246 }
247 let node = |id: i32| {
248 metadata
249 .brokers
250 .iter()
251 .find(|broker| broker.node_id == id)
252 .map_or_else(
253 || Node {
254 id,
255 host: String::new(),
256 port: -1,
257 rack: None,
258 },
259 |broker| Node {
260 id,
261 host: broker.host.clone(),
262 port: broker.port,
263 rack: broker.rack.clone(),
264 },
265 )
266 };
267 let nodes = |ids: &[i32]| ids.iter().map(|id| node(*id)).collect::<Vec<_>>();
268 Ok(Some(
269 metadata
270 .topics
271 .iter()
272 .filter(|topic| topic.error_code == 0)
273 .filter_map(|topic| topic.name.as_ref().map(|name| (name, topic)))
274 .map(|(name, topic)| {
275 let partitions = topic
276 .partitions
277 .iter()
278 .map(|partition| PartitionInfo {
279 topic: name.clone(),
280 partition: partition.partition_index,
281 leader: metadata
282 .brokers
283 .iter()
284 .any(|broker| broker.node_id == partition.leader_id)
285 .then(|| node(partition.leader_id)),
286 replicas: nodes(&partition.replica_nodes),
287 in_sync_replicas: nodes(&partition.isr_nodes),
288 offline_replicas: nodes(&partition.offline_replicas),
289 })
290 .collect();
291 (name.clone(), partitions)
292 })
293 .collect(),
294 ))
295}
296
297impl Consumer {
298 pub async fn beginning_offsets(
309 &self,
310 partitions: &[(String, i32)],
311 ) -> Result<HashMap<(String, i32), i64>, ConsumerError> {
312 self.boundary_offsets(partitions, poll::EARLIEST_TIMESTAMP)
313 .await
314 }
315
316 pub async fn end_offsets(
327 &self,
328 partitions: &[(String, i32)],
329 ) -> Result<HashMap<(String, i32), i64>, ConsumerError> {
330 self.boundary_offsets(partitions, poll::LATEST_TIMESTAMP)
331 .await
332 }
333
334 async fn boundary_offsets(
335 &self,
336 partitions: &[(String, i32)],
337 timestamp: i64,
338 ) -> Result<HashMap<(String, i32), i64>, ConsumerError> {
339 let search = partitions
340 .iter()
341 .map(|partition| (partition.clone(), timestamp))
342 .collect();
343 Ok(self
344 .search_offsets(search, false)
345 .await?
346 .into_iter()
347 .map(|(partition, found)| (partition, found.offset))
348 .collect())
349 }
350
351 pub async fn offsets_for_times(
363 &self,
364 timestamps: &HashMap<(String, i32), i64>,
365 ) -> Result<HashMap<(String, i32), Option<OffsetAndTimestamp>>, ConsumerError> {
366 if let Some(((topic, partition), timestamp)) =
367 timestamps.iter().find(|(_, timestamp)| **timestamp < 0)
368 {
369 return Err(ConsumerError::InvalidArgument(format!(
370 "The target time for partition {topic}-{partition} is {timestamp}. The target time cannot be negative."
371 )));
372 }
373 let mut found = self.search_offsets(timestamps.clone(), true).await?;
374 Ok(timestamps
375 .keys()
376 .map(|partition| (partition.clone(), found.remove(partition)))
377 .collect())
378 }
379
380 pub async fn current_lag(
391 &self,
392 topic: impl Into<String>,
393 partition: i32,
394 ) -> Result<Option<i64>, ConsumerError> {
395 let key = (topic.into(), partition);
396 let assigned = self.assigned.lock().await;
397 if !assigned.contains(&key) {
398 let (topic, partition) = key;
399 return Err(ConsumerError::NoCurrentAssignment { topic, partition });
400 }
401 let offsets = self.next_offsets.lock().await;
402 let awaiting_validation = self
405 .positions
406 .lock()
407 .await
408 .get(&key)
409 .is_some_and(|position| position.awaiting_validation);
410 let position = offsets
411 .get(&key)
412 .copied()
413 .filter(|offset| !is_reset_sentinel(*offset) && !awaiting_validation);
414 drop(offsets);
415 drop(assigned);
416 let end = self.end_offsets.lock().await.get(&key).copied();
417 Ok(position.zip(end).map(|(position, end)| end - position))
418 }
419
420 async fn search_offsets(
430 &self,
431 search: HashMap<(String, i32), i64>,
432 require_timestamps: bool,
433 ) -> Result<HashMap<(String, i32), OffsetAndTimestamp>, ConsumerError> {
434 let started = Instant::now();
435 let deadline = started + self.default_api_timeout.to_std();
436 timeout_at(
439 deadline,
440 self.search_offsets_until(search, require_timestamps, started, deadline),
441 )
442 .await
443 .unwrap_or_else(|_| {
444 Err(ConsumerError::timeout(format!(
445 "Failed to get offsets by times in {}ms",
446 started.elapsed().as_millis()
447 )))
448 })
449 }
450
451 async fn search_offsets_until(
452 &self,
453 mut search: HashMap<(String, i32), i64>,
454 require_timestamps: bool,
455 started: Instant,
456 deadline: Instant,
457 ) -> Result<HashMap<(String, i32), OffsetAndTimestamp>, ConsumerError> {
458 let mut backoff = self.retry_policy.initial_backoff;
459 let mut found = HashMap::new();
460 while !search.is_empty() {
461 let topics: BTreeSet<String> = search.keys().map(|(topic, _)| topic.clone()).collect();
462 let request = krabka_client_core::topics_request(
463 topics.iter().cloned(),
464 self.allows_auto_topic_creation(),
465 );
466 match self.client.refresh_metadata_with(request).await {
467 Ok(metadata) => {
468 requested_topic_errors(&metadata, &topics)?;
469 let leaders = partition_leaders(&metadata);
470 let mut by_leader: BTreeMap<i32, Vec<QueryPartition>> = BTreeMap::new();
471 for (partition, timestamp) in &search {
472 if let Some((leader, epoch)) = leaders.get(partition)
473 && self.client.knows_broker(*leader)
474 {
475 by_leader.entry(*leader).or_default().push((
476 partition.clone(),
477 *epoch,
478 *timestamp,
479 ));
480 }
481 }
482 let mut unauthorized = BTreeSet::new();
483 for (leader, partitions) in by_leader {
484 let request = query_request(&partitions, self.isolation_level);
485 let broker = self.client.broker(leader);
486 let answer = if self.isolation_level == IsolationLevel::ReadCommitted {
490 broker
491 .send(poll::ReadCommittedListOffsets::new(request))
492 .await
493 } else if require_timestamps {
494 broker.send(TimestampListOffsets(request)).await
495 } else {
496 broker.send(request).await
497 };
498 match answer {
499 Ok(answer) => {
500 apply_query_answer(
501 &answer,
502 &mut search,
503 &mut found,
504 &mut unauthorized,
505 );
506 }
507 Err(error) if poll::is_transient_transport_error(&error) => {
508 self.client.evict_broker(leader);
509 }
510 Err(error) => return Err(error.into()),
511 }
512 }
513 if !unauthorized.is_empty() {
514 return Err(ConsumerError::TopicAuthorizationFailed(unauthorized));
515 }
516 }
517 Err(error) if poll::is_transient_transport_error(&error) => {}
518 Err(error) => return Err(error.into()),
519 }
520 if search.is_empty() {
521 break;
522 }
523 if Instant::now() >= deadline {
524 return Err(ConsumerError::timeout(format!(
525 "Failed to get offsets by times in {}ms",
526 started.elapsed().as_millis()
527 )));
528 }
529 sleep_until((Instant::now() + backoff).min(deadline)).await;
530 backoff = next_backoff(backoff, self.retry_policy.max_backoff);
531 }
532 Ok(found)
533 }
534
535 pub async fn partitions_for(&self, topic: &str) -> Result<Vec<PartitionInfo>, ConsumerError> {
546 let request = krabka_client_core::topics_request(
547 [topic.to_owned()],
548 self.allows_auto_topic_creation(),
549 );
550 Ok(self
551 .topic_metadata(request)
552 .await?
553 .remove(topic)
554 .unwrap_or_default())
555 }
556
557 pub async fn list_topics(&self) -> Result<HashMap<String, Vec<PartitionInfo>>, ConsumerError> {
566 self.topic_metadata(krabka_client_core::all_topics_request())
567 .await
568 }
569
570 fn allows_auto_topic_creation(&self) -> bool {
572 matches!(
573 self.client.metadata_topics().scope(),
574 MetadataScope::Topics {
575 allow_auto_topic_creation: true
576 }
577 )
578 }
579
580 async fn topic_metadata(
583 &self,
584 request: MetadataRequest,
585 ) -> Result<HashMap<String, Vec<PartitionInfo>>, ConsumerError> {
586 let deadline = Instant::now() + self.default_api_timeout.to_std();
587 timeout_at(deadline, self.topic_metadata_until(request, deadline))
588 .await
589 .unwrap_or_else(|_| {
590 Err(ConsumerError::timeout(
591 "Timeout expired while fetching topic metadata".to_owned(),
592 ))
593 })
594 }
595
596 async fn topic_metadata_until(
597 &self,
598 request: MetadataRequest,
599 deadline: Instant,
600 ) -> Result<HashMap<String, Vec<PartitionInfo>>, ConsumerError> {
601 let mut backoff = self.retry_policy.initial_backoff;
602 loop {
603 match self.client.refresh_metadata_with(request.clone()).await {
604 Ok(metadata) => {
605 if let Some(topics) = topic_partition_infos(&metadata)? {
606 return Ok(topics);
607 }
608 }
609 Err(error) if poll::is_transient_transport_error(&error) => {}
610 Err(error) => return Err(error.into()),
611 }
612 if Instant::now() >= deadline {
613 return Err(ConsumerError::timeout(
614 "Timeout expired while fetching topic metadata".to_owned(),
615 ));
616 }
617 sleep_until((Instant::now() + backoff).min(deadline)).await;
618 backoff = next_backoff(backoff, self.retry_policy.max_backoff);
619 }
620 }
621}
622
623fn apply_query_answer(
625 answer: &ListOffsetsResponse,
626 search: &mut HashMap<(String, i32), i64>,
627 found: &mut HashMap<(String, i32), OffsetAndTimestamp>,
628 unauthorized: &mut BTreeSet<String>,
629) {
630 for topic in &answer.topics {
631 for row in &topic.partitions {
632 let key = (topic.name.clone(), row.partition_index);
633 if !search.contains_key(&key) {
634 continue;
635 }
636 match classify_query_row(row.error_code, row.offset, row.timestamp, row.leader_epoch) {
637 QueryRow::Found(offset) => {
638 search.remove(&key);
639 found.insert(key, offset);
640 }
641 QueryRow::NotFound => {
642 search.remove(&key);
643 }
644 QueryRow::Unauthorized => {
645 unauthorized.insert(topic.name.clone());
646 }
647 QueryRow::Retry => {}
648 }
649 }
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use std::{
656 collections::HashMap,
657 sync::{
658 Arc, Mutex,
659 atomic::{AtomicU16, Ordering},
660 },
661 };
662
663 use krabka_client_core::{Client, MockBroker};
664 use krabka_protocol::{
665 Decode as _, Encode,
666 owned::{
667 api_versions_request,
668 api_versions_response::{ApiVersion, ApiVersionsResponse},
669 list_offsets_request::{
670 self, ListOffsetsPartition, ListOffsetsRequest, ListOffsetsTopic,
671 },
672 list_offsets_response::{
673 ListOffsetsPartitionResponse, ListOffsetsResponse, ListOffsetsTopicResponse,
674 },
675 metadata_request,
676 metadata_response::{
677 MetadataResponse, MetadataResponseBroker, MetadataResponsePartition,
678 MetadataResponseTopic,
679 },
680 },
681 };
682 use krabka_units::{millis, secs};
683 use poll::partition_error_tests::consumer_with_client;
684
685 use super::*;
686
687 type Rows = fn(i32, i64) -> (i16, i64, i64, i32);
690
691 type SentRequests = Arc<Mutex<Vec<ListOffsetsRequest>>>;
692
693 fn encode(response: &impl Encode, version: i16) -> Vec<u8> {
694 let mut buf = bytes::BytesMut::new();
695 response.encode(&mut buf, version).expect("encode");
696 buf.to_vec()
697 }
698
699 fn metadata(port: i32, error_code: i16) -> MetadataResponse {
700 MetadataResponse {
701 brokers: vec![MetadataResponseBroker {
702 node_id: 1,
703 host: "127.0.0.1".into(),
704 port,
705 rack: Some("az-1".into()),
706 ..Default::default()
707 }],
708 topics: vec![MetadataResponseTopic {
709 error_code,
710 name: Some("orders".into()),
711 partitions: (0..2)
712 .map(|partition_index| MetadataResponsePartition {
713 partition_index,
714 leader_id: 1,
715 leader_epoch: 4,
716 replica_nodes: vec![1, 2],
717 isr_nodes: vec![1],
718 offline_replicas: vec![2],
719 ..Default::default()
720 })
721 .collect(),
722 ..Default::default()
723 }],
724 ..Default::default()
725 }
726 }
727
728 async fn query_broker(
732 rows: Option<Rows>,
733 metadata_error: i16,
734 list_offsets_max_version: i16,
735 sent: SentRequests,
736 ) -> MockBroker {
737 use bytes::Buf as _;
738 let port = Arc::new(AtomicU16::new(0));
739 let port_in_mock = Arc::clone(&port);
740 let broker = MockBroker::start(move |api_key, version, _corr_id, mut body| {
741 if api_key == api_versions_request::API_KEY {
742 let versions = ApiVersionsResponse {
743 api_keys: [
744 (api_versions_request::API_KEY, 3),
745 (metadata_request::API_KEY, 8),
746 (list_offsets_request::API_KEY, list_offsets_max_version),
747 ]
748 .into_iter()
749 .map(|(api_key, max_version)| ApiVersion {
750 api_key,
751 min_version: 0,
752 max_version,
753 ..Default::default()
754 })
755 .collect(),
756 ..Default::default()
757 };
758 return Some(encode(&versions, 0));
759 }
760 if api_key == metadata_request::API_KEY {
761 let port = i32::from(port_in_mock.load(Ordering::SeqCst));
762 return Some(encode(&metadata(port, metadata_error), version));
763 }
764 if api_key == list_offsets_request::API_KEY {
765 let client_id_len = usize::try_from(body.get_i16()).expect("client id");
766 body.advance(client_id_len);
767 let request = ListOffsetsRequest::decode(&mut body, version).expect("decode");
768 sent.lock().expect("sent lock").push(request.clone());
769 let rows = rows?;
770 let answer = ListOffsetsResponse {
771 topics: request
772 .topics
773 .iter()
774 .map(|topic| ListOffsetsTopicResponse {
775 name: topic.name.clone(),
776 partitions: topic
777 .partitions
778 .iter()
779 .map(|partition| {
780 let (error_code, offset, timestamp, leader_epoch) =
781 rows(partition.partition_index, partition.timestamp);
782 ListOffsetsPartitionResponse {
783 partition_index: partition.partition_index,
784 error_code,
785 offset,
786 timestamp,
787 leader_epoch,
788 ..Default::default()
789 }
790 })
791 .collect(),
792 ..Default::default()
793 })
794 .collect(),
795 ..Default::default()
796 };
797 return Some(encode(&answer, version));
798 }
799 None
800 })
801 .await
802 .unwrap();
803 port.store(broker.addr.port(), Ordering::SeqCst);
804 broker
805 }
806
807 fn request(isolation_level: i8, timestamps: [i64; 2]) -> ListOffsetsRequest {
808 ListOffsetsRequest {
809 replica_id: -1,
810 isolation_level,
811 topics: vec![ListOffsetsTopic {
812 name: "orders".into(),
813 partitions: timestamps
814 .into_iter()
815 .zip(0..)
816 .map(|(timestamp, partition_index)| ListOffsetsPartition {
817 partition_index,
818 current_leader_epoch: 4,
819 timestamp,
820 ..Default::default()
821 })
822 .collect(),
823 ..Default::default()
824 }],
825 ..Default::default()
826 }
827 }
828
829 #[derive(Debug, PartialEq)]
831 enum Answer {
832 Offsets(HashMap<(String, i32), i64>),
833 Times(HashMap<(String, i32), Option<OffsetAndTimestamp>>),
834 Error(String),
835 }
836
837 #[derive(Clone, Copy, Debug)]
838 enum Query {
839 Beginning,
840 End,
841 Times([i64; 2]),
842 }
843
844 #[tokio::test]
849 async fn offset_queries_follow_kafkas_offset_fetcher() {
850 let key = |partition: i32| ("orders".to_owned(), partition);
851 let found: Rows = |partition, timestamp| {
852 if timestamp < 0 {
853 (0, i64::from(partition) + 3, -1, -1)
854 } else if partition == 0 {
855 (0, 42, timestamp + 5, 2)
856 } else {
857 (0, -1, -1, -1)
858 }
859 };
860 let unauthorized: Rows = |_, _| (29, -1, -1, -1);
861 let mut actual = Vec::new();
862 let mut wanted = Vec::new();
863 for (name, isolation, query, (rows, metadata_error, list_offsets_max), expected, expected_requests) in [
864 (
865 "beginning offsets",
866 IsolationLevel::ReadUncommitted,
867 Query::Beginning,
868 (Some(found), 0, 5),
869 Answer::Offsets(maplit::hashmap! {key(0) => 3, key(1) => 4}),
870 vec![request(0, [-2, -2])],
871 ),
872 (
873 "end offsets with read committed",
874 IsolationLevel::ReadCommitted,
875 Query::End,
876 (Some(found), 0, 5),
877 Answer::Offsets(maplit::hashmap! {key(0) => 3, key(1) => 4}),
878 vec![request(1, [-1, -1])],
879 ),
880 (
881 "offsets for times",
882 IsolationLevel::ReadUncommitted,
883 Query::Times([1000, 2000]),
884 (Some(found), 0, 5),
885 Answer::Times(maplit::hashmap! {key(0) => Some(OffsetAndTimestamp {
886 offset: 42,
887 timestamp: 1005,
888 leader_epoch: Some(2),
889 }), key(1) => None}),
890 vec![request(0, [1000, 2000])],
891 ),
892 (
893 "negative timestamp",
894 IsolationLevel::ReadUncommitted,
895 Query::Times([-1, 2000]),
896 (Some(found), 0, 5),
897 Answer::Error(
898 "invalid argument: The target time for partition orders-0 is -1. The target time cannot be negative."
899 .into(),
900 ),
901 vec![],
902 ),
903 (
904 "unauthorized topic",
905 IsolationLevel::ReadUncommitted,
906 Query::Beginning,
907 (Some(unauthorized), 0, 5),
908 Answer::Error("not authorized to access topics: [orders]".into()),
909 vec![request(0, [-2, -2])],
910 ),
911 (
912 "metadata does not authorize the topic",
913 IsolationLevel::ReadUncommitted,
914 Query::Beginning,
915 (Some(found), 29, 5),
916 Answer::Error("not authorized to access topics: [orders]".into()),
917 vec![],
918 ),
919 (
920 "metadata names the topic invalid",
921 IsolationLevel::ReadUncommitted,
922 Query::End,
923 (Some(found), 17, 5),
924 Answer::Error("topic 'orders' is invalid".into()),
925 vec![],
926 ),
927 (
928 "a silent leader ends by the API timeout",
929 IsolationLevel::ReadUncommitted,
930 Query::Times([1000, 2000]),
931 (None, 0, 5),
932 Answer::Error("timeout".into()),
933 vec![request(0, [1000, 2000])],
934 ),
935 (
936 "offsets for times on a broker with ListOffsets v1",
937 IsolationLevel::ReadUncommitted,
938 Query::Times([1000, 2000]),
939 (Some(found), 0, 1),
940 Answer::Times(maplit::hashmap! {key(0) => Some(OffsetAndTimestamp {
941 offset: 42,
942 timestamp: 1005,
943 leader_epoch: None,
944 }), key(1) => None}),
945 vec![ListOffsetsRequest {
946 replica_id: -1,
947 topics: vec![ListOffsetsTopic {
948 name: "orders".into(),
949 partitions: [1000, 2000]
950 .into_iter()
951 .zip(0..)
952 .map(|(timestamp, partition_index)| ListOffsetsPartition {
953 partition_index,
954 current_leader_epoch: -1,
955 timestamp,
956 ..Default::default()
957 })
958 .collect(),
959 ..Default::default()
960 }],
961 ..Default::default()
962 }],
963 ),
964 ] {
965 let sent = SentRequests::default();
966 let broker =
967 query_broker(rows, metadata_error, list_offsets_max, Arc::clone(&sent)).await;
968 let client = Client::builder()
969 .bootstrap(broker.addr.to_string())
970 .request_timeout(secs(30))
971 .build()
972 .await
973 .expect("client");
974 let mut consumer = consumer_with_client(client);
975 consumer.isolation_level = isolation;
976 consumer.default_api_timeout = millis(500);
977 let partitions = [key(0), key(1)];
978 let answer = match query {
979 Query::Beginning => consumer.beginning_offsets(&partitions).await.map(Answer::Offsets),
980 Query::End => consumer.end_offsets(&partitions).await.map(Answer::Offsets),
981 Query::Times(timestamps) => consumer
982 .offsets_for_times(&partitions.iter().cloned().zip(timestamps).collect())
983 .await
984 .map(Answer::Times),
985 }
986 .unwrap_or_else(|error| match error {
987 ConsumerError::Timeout { .. } => Answer::Error("timeout".into()),
989 error => Answer::Error(error.to_string()),
990 });
991 broker.stop();
992 let mut requests = sent.lock().expect("sent lock").clone();
993 for request in &mut requests {
994 for topic in &mut request.topics {
995 topic.partitions.sort_by_key(|partition| partition.partition_index);
996 }
997 }
998 actual.push((name, answer, requests));
999 wanted.push((name, expected, expected_requests));
1000 }
1001 assert2::assert!(actual == wanted);
1002 }
1003
1004 #[tokio::test]
1007 async fn client_instance_id_needs_metrics_push() {
1008 let broker = query_broker(None, 0, 5, SentRequests::default()).await;
1009 let client = Client::builder()
1010 .bootstrap(broker.addr.to_string())
1011 .build()
1012 .await
1013 .expect("client");
1014 let consumer = consumer_with_client(client);
1015
1016 let id = consumer
1017 .client_instance_id(millis(100))
1018 .await
1019 .map_err(|error| error.to_string());
1020
1021 broker.stop();
1022 assert2::assert!(
1023 id == Err(
1024 "client: Telemetry is not enabled. Set config `enable.metrics.push` to `true`."
1025 .to_owned()
1026 )
1027 );
1028 }
1029
1030 #[tokio::test]
1036 async fn retriable_errors_retry_until_the_api_timeout() {
1037 let sent = SentRequests::default();
1038 let offsets_broker =
1039 query_broker(Some(|_, _| (6, -1, -1, -1)), 0, 5, Arc::clone(&sent)).await;
1040 let metadata_broker = query_broker(None, 5, 5, SentRequests::default()).await;
1041 let mut answers = Vec::new();
1042 for broker in [&offsets_broker, &metadata_broker] {
1043 let client = Client::builder()
1044 .bootstrap(broker.addr.to_string())
1045 .request_timeout(secs(30))
1046 .build()
1047 .await
1048 .expect("client");
1049 let mut consumer = consumer_with_client(client);
1050 consumer.default_api_timeout = millis(300);
1051 let answer = if answers.is_empty() {
1052 consumer
1053 .end_offsets(&[("orders".to_owned(), 0)])
1054 .await
1055 .map(|_| ())
1056 } else {
1057 consumer.list_topics().await.map(|_| ())
1058 };
1059 answers.push(matches!(answer, Err(ConsumerError::Timeout { .. })));
1060 }
1061 offsets_broker.stop();
1062 metadata_broker.stop();
1063 let list_offsets_requests = sent.lock().expect("sent lock").len();
1064 assert2::assert!((answers, list_offsets_requests > 1) == (vec![true, true], true));
1065 }
1066
1067 #[test]
1072 fn topic_partition_infos_follow_kafkas_topic_metadata_fetcher() {
1073 let node_1 = Node {
1074 id: 1,
1075 host: "127.0.0.1".into(),
1076 port: 9092,
1077 rack: Some("az-1".into()),
1078 };
1079 let node_2 = Node {
1080 id: 2,
1081 host: String::new(),
1082 port: -1,
1083 rack: None,
1084 };
1085 let orders = (0..2)
1086 .map(|partition| PartitionInfo {
1087 topic: "orders".into(),
1088 partition,
1089 leader: Some(node_1.clone()),
1090 replicas: vec![node_1.clone(), node_2.clone()],
1091 in_sync_replicas: vec![node_1.clone()],
1092 offline_replicas: vec![node_2.clone()],
1093 })
1094 .collect::<Vec<_>>();
1095 let mut actual = Vec::new();
1096 let mut wanted = Vec::new();
1097 for (name, error_code, expected) in [
1098 (
1099 "no error",
1100 0,
1101 Ok(Some(maplit::hashmap! {"orders".to_owned() => orders})),
1102 ),
1103 ("unknown topic", 3, Ok(Some(HashMap::new()))),
1104 ("leader not available", 5, Ok(None)),
1105 (
1106 "unauthorized",
1107 29,
1108 Err("not authorized to access topics: [orders]".to_owned()),
1109 ),
1110 (
1111 "invalid topic",
1112 17,
1113 Err("topic 'orders' is invalid".to_owned()),
1114 ),
1115 ] {
1116 let result = topic_partition_infos(&metadata(9092, error_code))
1117 .map_err(|error| error.to_string());
1118 actual.push((name, result));
1119 wanted.push((name, expected));
1120 }
1121 assert2::assert!(actual == wanted);
1122 }
1123
1124 #[tokio::test]
1131 async fn list_topics_asks_for_every_topic() {
1132 use bytes::Buf as _;
1133 let seen: Arc<Mutex<Vec<MetadataRequest>>> = Arc::default();
1134 let seen_in_mock = Arc::clone(&seen);
1135 let port = Arc::new(AtomicU16::new(0));
1136 let port_in_mock = Arc::clone(&port);
1137 let broker = MockBroker::start(move |api_key, version, _corr_id, mut body| {
1138 if api_key == api_versions_request::API_KEY {
1139 let versions = ApiVersionsResponse {
1140 api_keys: [
1141 (api_versions_request::API_KEY, 3),
1142 (metadata_request::API_KEY, 8),
1143 ]
1144 .into_iter()
1145 .map(|(api_key, max_version)| ApiVersion {
1146 api_key,
1147 min_version: 0,
1148 max_version,
1149 ..Default::default()
1150 })
1151 .collect(),
1152 ..Default::default()
1153 };
1154 return Some(encode(&versions, 0));
1155 }
1156 if api_key == metadata_request::API_KEY {
1157 let client_id_len = usize::try_from(body.get_i16()).expect("client id");
1158 body.advance(client_id_len);
1159 let request = MetadataRequest::decode(&mut body, version).expect("decode");
1160 seen_in_mock.lock().expect("seen lock").push(request);
1161 let port = i32::from(port_in_mock.load(Ordering::SeqCst));
1162 return Some(encode(&metadata(port, 0), version));
1163 }
1164 None
1165 })
1166 .await
1167 .unwrap();
1168 port.store(broker.addr.port(), Ordering::SeqCst);
1169 let client = Client::builder()
1170 .bootstrap(broker.addr.to_string())
1171 .build()
1172 .await
1173 .expect("client");
1174 let consumer = consumer_with_client(client);
1175
1176 let topics = consumer.list_topics().await.expect("list topics");
1177
1178 assert2::check!(topics.keys().collect::<Vec<_>>() == vec!["orders"]);
1179 let last = seen.lock().expect("seen lock").last().cloned();
1180 assert2::check!(last == Some(krabka_client_core::all_topics_request()));
1181 assert2::check!(
1182 krabka_client_core::all_topics_request()
1183 == MetadataRequest {
1184 topics: None,
1185 allow_auto_topic_creation: true,
1186 ..Default::default()
1187 }
1188 );
1189 }
1190
1191 #[tokio::test]
1192 async fn current_lag_follows_kafkas_partition_lag() {
1193 let broker = query_broker(Some(|_, _| (0, 0, 0, 0)), 0, 5, SentRequests::default()).await;
1194 let client = Client::builder()
1195 .bootstrap(broker.addr.to_string())
1196 .build()
1197 .await
1198 .expect("client");
1199 let consumer = consumer_with_client(client);
1200 let key = ("orders".to_owned(), 0);
1201 let mut actual = Vec::new();
1202 let mut wanted = Vec::new();
1203 for (name, partition, position, end, seek, awaiting_validation, expected) in [
1204 ("known", 0, 5, Some(12), None, false, Ok(Some(7))),
1205 ("after a seek", 0, 5, Some(12), Some(8), false, Ok(Some(4))),
1206 ("no end offset", 0, 5, None, None, false, Ok(None)),
1207 ("awaiting validation", 0, 5, Some(12), None, true, Ok(None)),
1208 (
1209 "awaiting reset",
1210 0,
1211 poll::END_SENTINEL,
1212 Some(12),
1213 None,
1214 false,
1215 Ok(None),
1216 ),
1217 (
1218 "unassigned",
1219 9,
1220 5,
1221 Some(12),
1222 None,
1223 false,
1224 Err("no current assignment for partition orders-9".to_owned()),
1225 ),
1226 ] {
1227 consumer
1228 .positions
1229 .lock()
1230 .await
1231 .entry(key.clone())
1232 .or_default()
1233 .awaiting_validation = awaiting_validation;
1234 consumer
1235 .next_offsets
1236 .lock()
1237 .await
1238 .insert(key.clone(), position);
1239 let mut ends = consumer.end_offsets.lock().await;
1240 ends.clear();
1241 if let Some(end) = end {
1242 ends.insert(key.clone(), end);
1243 }
1244 drop(ends);
1245 if let Some(offset) = seek {
1246 consumer.seek("orders", 0, offset).await.expect("seek");
1247 }
1248 let lag = consumer
1249 .current_lag("orders", partition)
1250 .await
1251 .map_err(|error| error.to_string());
1252 actual.push((name, lag));
1253 wanted.push((name, expected));
1254 }
1255 broker.stop();
1256 assert2::assert!(actual == wanted);
1257 }
1258
1259 #[test]
1260 fn classify_query_row_follows_handle_list_offset_response() {
1261 let actual = [
1262 (0, 7, 100, 3),
1263 (0, 7, 100, -1),
1264 (0, -1, -1, -1),
1265 (43, -1, -1, -1),
1266 (29, -1, -1, -1),
1267 (6, -1, -1, -1),
1268 ]
1269 .map(|(error_code, offset, timestamp, epoch)| {
1270 classify_query_row(error_code, offset, timestamp, epoch)
1271 });
1272 let found = |leader_epoch| {
1273 QueryRow::Found(OffsetAndTimestamp {
1274 offset: 7,
1275 timestamp: 100,
1276 leader_epoch,
1277 })
1278 };
1279 assert2::assert!(
1280 actual
1281 == [
1282 found(Some(3)),
1283 found(None),
1284 QueryRow::NotFound,
1285 QueryRow::NotFound,
1286 QueryRow::Unauthorized,
1287 QueryRow::Retry,
1288 ]
1289 );
1290 }
1291}