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