1use {
40 async_trait::async_trait,
41 bytes::Bytes,
42 futures_util::stream::{StreamExt, unfold},
43 krabka_client_core::{
44 BrokerInfo, BrokerPool, Client, ClientError, ClientFrameMax, Connection,
45 ConnectionDispatchQueueCapacity, ConnectionOptions, FetchMinBytes, FetchPartitionResult,
46 IsolatedFetch, MetadataScope, connection_target_host,
47 fetch_partition_with_isolation_progress,
48 },
49 krabka_client_producer::{Acks, Producer, ProducerRecord},
50 krabka_protocol::{
51 owned::list_offsets_request::{ListOffsetsPartition, ListOffsetsRequest, ListOffsetsTopic},
52 primitives::uuid::Uuid as WireUuid,
53 },
54 krabka_units::prelude::{ByteSize, Time, TimeExt as _},
55 std::{
56 collections::{BTreeMap, HashMap},
57 sync::{Arc, Mutex as StdMutex},
58 },
59 tracing::{instrument, warn},
60};
61
62mod config;
63mod consumer;
64mod range;
65mod topic;
66
67pub use self::config::{
68 DEFAULT_METADATA_EVENT_QUEUE_CAPACITY, DEFAULT_METADATA_FETCH_MAX_BYTES,
69 DEFAULT_METADATA_FETCH_MAX_WAIT, DEFAULT_METADATA_FETCH_RETRY_BACKOFF,
70 DEFAULT_METADATA_TOPIC_CREATE_TIMEOUT, DEFAULT_MIN_ISR, DEFAULT_NUM_PARTITIONS,
71 DEFAULT_REPLICATION, KafkaMetadataLogConfig, METADATA_TOPIC, MetadataEventQueueCapacity,
72};
73
74fn connection_options(cfg: &KafkaMetadataLogConfig, purpose: &str) -> ConnectionOptions {
75 ConnectionOptions {
76 client_id: format!("{}-{purpose}", cfg.client_id),
77 dispatch_queue_capacity: cfg.dispatch_queue_capacity,
78 frame_max: cfg.frame_max,
79 security: cfg.security.clone().map(Box::new),
80 ..ConnectionOptions::default()
81 }
82}
83
84use {
85 self::{
86 consumer::{
87 ConsumerState, KafkaAssignmentHandle, MetadataEventChannel, metadata_event_channel,
88 },
89 range::{RangeFetcher, RangeReadFailure, RangeTopic, visit_range_pages},
90 topic::{TopicInfo, ensure_topic},
91 },
92 crate::{
93 error::MetadataLogError,
94 log::{
95 AssignmentHandle, KeyedEvent, MetadataEventLog, MetadataEventStream, PartitionStart,
96 RangeVisitor,
97 },
98 },
99 krabka_client_core::{security::ClientSecurity, transport::resolve},
100 krabka_protocol::owned::metadata_response::MetadataResponse,
101 std::net::SocketAddr,
102 tokio::{sync::Mutex, time},
103};
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107enum Access {
108 ReadWrite,
111 ReadOnly,
113}
114
115pub struct KafkaMetadataEventLog {
117 producer: Option<Producer>,
119 client: Client,
120 readers: BrokerPool,
122 topic: String,
123 topic_id: WireUuid,
124 partition_count: i32,
125 bootstrap: String,
126 client_id: String,
127 security: Option<ClientSecurity>,
128 fetch_max_wait: Time,
129 fetch_max_bytes: ByteSize,
130 fetch_retry_backoff: Time,
131 event_queue_capacity: MetadataEventQueueCapacity,
132 dispatch_queue_capacity: ConnectionDispatchQueueCapacity,
133 frame_max: ClientFrameMax,
134 subscriptions: Mutex<Vec<Arc<ConsumerState>>>,
135}
136
137impl KafkaMetadataEventLog {
138 #[instrument(skip_all, fields(topic = %cfg.topic, bootstrap = %cfg.bootstrap), err)]
146 pub async fn start(cfg: KafkaMetadataLogConfig) -> Result<Arc<Self>, MetadataLogError> {
147 Box::pin(Self::open(cfg, Access::ReadWrite)).await
148 }
149
150 #[instrument(skip_all, fields(topic = %cfg.topic, bootstrap = %cfg.bootstrap), err)]
164 pub async fn open_read_only(
165 mut cfg: KafkaMetadataLogConfig,
166 ) -> Result<Arc<Self>, MetadataLogError> {
167 cfg.provision_topic = false;
168 cfg.compacted = false;
169 Box::pin(Self::open(cfg, Access::ReadOnly)).await
170 }
171
172 async fn open(
173 cfg: KafkaMetadataLogConfig,
174 access: Access,
175 ) -> Result<Arc<Self>, MetadataLogError> {
176 cfg.validate()
177 .map_err(|error| MetadataLogError::Other(format!("invalid config: {error}")))?;
178
179 let TopicInfo {
183 partition_count,
184 topic_id,
185 } = ensure_topic(&cfg).await?;
186
187 let producer = match access {
191 Access::ReadWrite => Some(
192 Producer::builder()
193 .bootstrap(cfg.bootstrap.clone())
194 .client_id(format!("{}-producer", cfg.client_id))
195 .dispatch_queue_capacity(cfg.dispatch_queue_capacity.get())
196 .frame_max(cfg.frame_max.size())
197 .acks(Acks::All)
198 .enable_idempotence(true)
199 .maybe_security(cfg.security.clone())
200 .build()
201 .await
202 .map_err(|e| MetadataLogError::Other(format!("producer build failed: {e}")))?,
203 ),
204 Access::ReadOnly => None,
205 };
206
207 let client = Client::builder()
209 .bootstrap(cfg.bootstrap.clone())
210 .client_id(format!("{}-client", cfg.client_id))
211 .dispatch_queue_capacity(cfg.dispatch_queue_capacity.get())
212 .frame_max(cfg.frame_max.size())
213 .maybe_security(cfg.security.clone())
214 .metadata_scope(METADATA_TOPIC_SCOPE)
215 .build()
216 .await
217 .map_err(|e| MetadataLogError::Other(format!("client build failed: {e}")))?;
218 client.metadata_topics().set([cfg.topic.as_str()]);
219
220 let reader_options = connection_options(&cfg, "reader");
222 let readers = BrokerPool::new_with_server_names(
223 resolve_bootstrap(&cfg.bootstrap, &reader_options).await?,
224 reader_options,
225 );
226
227 Ok(Arc::new(Self {
228 producer,
229 client,
230 readers,
231 topic: cfg.topic,
232 topic_id,
233 partition_count,
234 bootstrap: cfg.bootstrap,
235 client_id: cfg.client_id,
236 security: cfg.security,
237 fetch_max_wait: cfg.fetch_max_wait,
238 fetch_max_bytes: cfg.fetch_max_bytes,
239 fetch_retry_backoff: cfg.fetch_retry_backoff,
240 event_queue_capacity: cfg.event_queue_capacity,
241 dispatch_queue_capacity: cfg.dispatch_queue_capacity,
242 frame_max: cfg.frame_max,
243 subscriptions: Mutex::new(Vec::new()),
244 }))
245 }
246
247 pub async fn shutdown(&self) {
250 let mut subs = self.subscriptions.lock().await;
251 for state in subs.drain(..) {
252 state.cancel_all();
253 }
254 }
255
256 async fn list_offsets(&self, timestamp: i64) -> Result<Vec<i64>, MetadataLogError> {
257 let metadata = self
258 .client
259 .refresh_metadata()
260 .await
261 .map_err(|e| MetadataLogError::Other(format!("Metadata failed: {e}")))?;
262 let mut by_leader: BTreeMap<i32, Vec<i32>> = BTreeMap::new();
263 for partition in 0..self.partition_count {
264 let leader = partition_leader(&metadata, &self.topic, partition).ok_or_else(|| {
265 MetadataLogError::Other(format!(
266 "{} partition {partition} has no available leader",
267 self.topic
268 ))
269 })?;
270 by_leader.entry(leader).or_default().push(partition);
271 }
272 let mut offsets = vec![0i64; usize_count(self.partition_count)?];
273 for (leader, partitions) in by_leader {
274 let req = ListOffsetsRequest {
275 replica_id: -1,
276 isolation_level: 0,
277 topics: vec![ListOffsetsTopic {
278 name: self.topic.clone(),
279 partitions: partitions
280 .into_iter()
281 .map(|partition_index| ListOffsetsPartition {
282 partition_index,
283 current_leader_epoch: -1,
284 timestamp,
285 ..Default::default()
286 })
287 .collect(),
288 ..Default::default()
289 }],
290 ..Default::default()
291 };
292 let resp = self.client.broker(leader).send(req).await.map_err(|e| {
293 MetadataLogError::Other(format!("ListOffsets from broker {leader} failed: {e}"))
294 })?;
295 for p in resp.topics.iter().flat_map(|topic| &topic.partitions) {
296 if p.error_code != 0 {
297 return Err(MetadataLogError::Other(format!(
298 "ListOffsets partition {} error {}",
299 p.partition_index, p.error_code
300 )));
301 }
302 if let Some(slot) = usize::try_from(p.partition_index)
303 .ok()
304 .and_then(|idx| offsets.get_mut(idx))
305 {
306 *slot = p.offset;
307 }
308 }
309 }
310 Ok(offsets)
311 }
312}
313
314async fn resolve_bootstrap(
317 bootstrap: &str,
318 options: &ConnectionOptions,
319) -> Result<Vec<(SocketAddr, String)>, MetadataLogError> {
320 let mut resolved = Vec::new();
321 for address in bootstrap.split(',').map(str::trim) {
322 if address.is_empty() {
323 continue;
324 }
325 let lookup = time::timeout(options.dns_timeout.time().to_std(), resolve(address))
326 .await
327 .map_err(|_elapsed| {
328 MetadataLogError::Other(format!("DNS lookup of {address} timed out"))
329 })?
330 .map_err(|error| {
331 MetadataLogError::Other(format!("DNS lookup of {address} failed: {error}"))
332 })?;
333 resolved.extend(
334 lookup
335 .into_iter()
336 .map(|socket| (socket, connection_target_host(address).to_owned())),
337 );
338 }
339 if resolved.is_empty() {
340 return Err(MetadataLogError::Other(format!(
341 "bootstrap {bootstrap:?} resolved to no address"
342 )));
343 }
344 Ok(resolved)
345}
346
347fn brokers_of(metadata: &MetadataResponse) -> Vec<BrokerInfo> {
349 metadata
350 .brokers
351 .iter()
352 .map(|broker| BrokerInfo {
353 id: broker.node_id,
354 host: broker.host.clone(),
355 port: broker.port,
356 rack: broker.rack.clone(),
357 })
358 .collect()
359}
360
361pub(super) const METADATA_TOPIC_SCOPE: MetadataScope = MetadataScope::Topics {
368 allow_auto_topic_creation: false,
369};
370
371pub(super) fn partition_leader(
372 metadata: &MetadataResponse,
373 topic: &str,
374 partition: i32,
375) -> Option<i32> {
376 metadata
377 .topics
378 .iter()
379 .find(|entry| entry.name.as_deref() == Some(topic) && entry.error_code == 0)?
380 .partitions
381 .iter()
382 .find(|entry| entry.partition_index == partition && entry.error_code == 0)
383 .map(|entry| entry.leader_id)
384 .filter(|leader| *leader >= 0)
385}
386
387impl Drop for KafkaMetadataEventLog {
388 fn drop(&mut self) {
389 if let Ok(mut subs) = self.subscriptions.try_lock() {
390 for state in subs.drain(..) {
391 state.cancel_all();
392 }
393 }
394 }
395}
396
397#[async_trait]
398impl MetadataEventLog for KafkaMetadataEventLog {
399 fn partition_count(&self) -> i32 { self.partition_count }
400
401 #[instrument(
402 level = "debug",
403 skip_all,
404 fields(topic = %self.topic, partition, len = event.len()),
405 err
406 )]
407 async fn publish(&self, partition: i32, event: Bytes) -> Result<i64, MetadataLogError> {
408 self.publish_record(partition, None, Some(event)).await
409 }
410
411 async fn publish_keyed(
412 &self,
413 partition: i32,
414 key: Bytes,
415 event: Option<Bytes>,
416 ) -> Result<i64, MetadataLogError> {
417 self.publish_record(partition, Some(key), event).await
418 }
419
420 async fn publish_keyed_group(
421 &self,
422 events: Vec<KeyedEvent>,
423 ) -> Result<Vec<i64>, MetadataLogError> {
424 self.publish_record_group(events).await
425 }
426
427 fn subscribe(
428 &self,
429 assignment: Vec<PartitionStart>,
430 ) -> (MetadataEventStream, Arc<dyn AssignmentHandle>) {
431 let MetadataEventChannel {
432 sender: tx,
433 receiver: rx,
434 } = metadata_event_channel(self.event_queue_capacity);
435 let state = Arc::new(ConsumerState {
436 bootstrap: self.bootstrap.clone(),
437 client_id: format!("{}-consumer", self.client_id),
438 security: self.security.clone(),
439 topic: self.topic.clone(),
440 topic_id: self.topic_id,
441 tx,
442 fetch_max_wait: self.fetch_max_wait,
443 fetch_max_bytes: self.fetch_max_bytes,
444 fetch_retry_backoff: self.fetch_retry_backoff,
445 dispatch_queue_capacity: self.dispatch_queue_capacity,
446 frame_max: self.frame_max,
447 tasks: StdMutex::new(HashMap::new()),
448 });
449 for ps in assignment {
450 state.spawn_partition(ps);
451 }
452 if let Ok(mut subs) = self.subscriptions.try_lock() {
453 subs.push(Arc::clone(&state));
454 } else {
455 warn!("KafkaMetadataEventLog: could not track subscription state");
456 }
457 let stream = unfold(rx, |mut rx| async move { rx.recv().await.map(|r| (r, rx)) }).boxed();
458 let handle: Arc<dyn AssignmentHandle> = Arc::new(KafkaAssignmentHandle { state });
459 (stream, handle)
460 }
461
462 async fn low_water_marks(&self) -> Result<Vec<i64>, MetadataLogError> {
463 self.list_offsets(-2).await }
465
466 #[instrument(
467 level = "debug",
468 skip_all,
469 fields(topic = %self.topic, partition_count = self.partition_count),
470 err
471 )]
472 async fn high_water_marks(&self) -> Result<Vec<i64>, MetadataLogError> {
473 self.list_offsets(-1).await }
475
476 #[instrument(
477 level = "debug",
478 skip_all,
479 fields(topic = %self.topic, partition, start, end),
480 err
481 )]
482 async fn visit_range(
483 &self,
484 partition: i32,
485 start: i64,
486 end: i64,
487 visit: &mut RangeVisitor<'_>,
488 ) -> Result<(), MetadataLogError> {
489 let topic = RangeTopic {
490 name: &self.topic,
491 partition_count: self.partition_count,
492 retry_backoff: self.fetch_retry_backoff.to_std(),
493 };
494 visit_range_pages(self, &topic, partition, start, end, visit).await
495 }
496}
497
498impl RangeFetcher for KafkaMetadataEventLog {
499 type Leader = Arc<Connection>;
500
501 #[cfg_attr(test, mutants::skip)]
503 async fn leader(&self, partition: i32) -> Result<Arc<Connection>, RangeReadFailure> {
504 self.leader_connection(partition).await
505 }
506
507 #[cfg_attr(test, mutants::skip)]
509 async fn fetch(
510 &self,
511 leader: &Arc<Connection>,
512 partition: i32,
513 offset: i64,
514 ) -> Result<FetchPartitionResult, ClientError> {
515 fetch_partition_with_isolation_progress(leader, IsolatedFetch {
516 topic: &self.topic,
517 topic_id: self.topic_id,
518 partition,
519 fetch_offset: offset,
520 max_wait: self.fetch_max_wait,
521 max: krabka_client_core::DEFAULT_FETCH_RESPONSE_MAX,
522 partition_max: self.fetch_max_bytes,
523 fetch_min: FetchMinBytes::default(),
524 isolation_level: 0,
525 })
526 .await
527 }
528}
529
530impl KafkaMetadataEventLog {
531 async fn publish_record(
532 &self,
533 partition: i32,
534 key: Option<Bytes>,
535 event: Option<Bytes>,
536 ) -> Result<i64, MetadataLogError> {
537 let record = producer_record(&self.topic, self.partition_count, partition, key, event)?;
538 let meta = self
539 .producer()?
540 .send(record)
541 .await
542 .map_err(|e| MetadataLogError::Publish(e.to_string()))?;
543 Ok(meta.offset)
544 }
545
546 async fn publish_record_group(
556 &self,
557 events: Vec<KeyedEvent>,
558 ) -> Result<Vec<i64>, MetadataLogError> {
559 self.publish_record_group_through(self.producer()?, events)
560 .await
561 }
562
563 fn producer(&self) -> Result<&Producer, MetadataLogError> {
565 self.producer
566 .as_ref()
567 .ok_or_else(|| MetadataLogError::Publish("the metadata log is read-only".into()))
568 }
569
570 async fn publish_record_group_through(
572 &self,
573 producer: &Producer,
574 events: Vec<KeyedEvent>,
575 ) -> Result<Vec<i64>, MetadataLogError> {
576 let mut failure = None;
577 let mut handles = Vec::with_capacity(events.len());
578 for KeyedEvent {
579 partition,
580 key,
581 event,
582 } in events
583 {
584 let enqueued = match producer_record(
585 &self.topic,
586 self.partition_count,
587 partition,
588 Some(key),
589 event,
590 ) {
591 Ok(record) => producer
592 .enqueue(record)
593 .await
594 .map_err(|e| MetadataLogError::Publish(e.to_string())),
595 Err(error) => Err(error),
596 };
597 match enqueued {
598 Ok(handle) => handles.push(handle),
599 Err(error) => {
600 failure = Some(error);
601 break;
602 }
603 }
604 }
605 let mut offsets = Vec::with_capacity(handles.len());
606 for handle in handles {
607 match handle.await {
608 Ok(meta) => offsets.push(meta.offset),
609 Err(error) => {
610 failure.get_or_insert_with(|| MetadataLogError::Publish(error.to_string()));
611 }
612 }
613 }
614 failure.map_or(Ok(offsets), Err)
615 }
616
617 async fn leader_connection(&self, partition: i32) -> Result<Arc<Connection>, RangeReadFailure> {
623 let metadata = self
624 .client
625 .refresh_metadata()
626 .await
627 .map_err(|error| RangeReadFailure::client("Metadata failed", &error))?;
628 let leader = partition_leader(&metadata, &self.topic, partition).ok_or_else(|| {
629 RangeReadFailure::retriable(format!(
630 "{} partition {partition} has no available leader",
631 self.topic
632 ))
633 })?;
634 self.readers.refresh_brokers(&brokers_of(&metadata)).await;
635 match self.readers.get(leader).await {
636 Err(ClientError::Disconnected) if !self.readers.knows_broker(leader) => {
637 self.readers.bootstrap_connection().await
638 }
639 connection => connection,
640 }
641 .map_err(|error| {
642 RangeReadFailure::client(&format!("connect to broker {leader} failed"), &error)
643 })
644 }
645}
646
647fn producer_record(
648 topic: &str,
649 partition_count: i32,
650 partition: i32,
651 key: Option<Bytes>,
652 event: Option<Bytes>,
653) -> Result<ProducerRecord, MetadataLogError> {
654 if partition < 0 || partition >= partition_count {
655 return Err(MetadataLogError::PartitionOutOfRange {
656 partition,
657 count: partition_count,
658 });
659 }
660 Ok(ProducerRecord {
661 topic: topic.to_owned(),
662 partition: Some(partition),
663 key,
664 value: event,
665 ..Default::default()
666 })
667}
668
669fn usize_count(n: i32) -> Result<usize, MetadataLogError> {
670 usize::try_from(n).map_err(|_| MetadataLogError::Other(format!("partition_count {n} negative")))
671}
672
673#[cfg(test)]
674mod tests {
675 use {
676 super::*,
677 assert2::{assert, check},
678 };
679
680 #[test]
681 fn metadata_requests_name_only_the_metadata_topic_without_auto_creation() {
682 use {
683 krabka_client_core::MetadataTopics,
684 krabka_protocol::owned::metadata_request::{MetadataRequest, MetadataRequestTopic},
685 };
686
687 let topics = MetadataTopics::new(METADATA_TOPIC_SCOPE);
688 topics.set([config::METADATA_TOPIC]);
689
690 check!(
691 topics.request()
692 == MetadataRequest {
693 topics: Some(vec![MetadataRequestTopic {
694 name: Some(config::METADATA_TOPIC.to_owned()),
695 ..Default::default()
696 }]),
697 allow_auto_topic_creation: false,
698 ..Default::default()
699 }
700 );
701 }
702
703 #[tokio::test]
704 async fn open_read_only_rejects_invalid_policy_before_connecting() {
705 let cfg = KafkaMetadataLogConfig {
706 topic_create_timeout: Time::ZERO,
707 ..KafkaMetadataLogConfig::new("not a socket address")
708 };
709
710 let error = KafkaMetadataEventLog::open_read_only(cfg)
711 .await
712 .err()
713 .expect("invalid policy must fail before network I/O");
714 assert!(error.to_string().contains("topic_create_timeout"));
715 }
716
717 #[yare::parameterized(
718 empty = { "", "bootstrap \"\" resolved to no address" },
719 separators_only = { " , ", "bootstrap \" , \" resolved to no address" },
720 missing_port = { "no-port", "DNS lookup of no-port failed" },
721 )]
722 #[test_macro(tokio::test)]
723 async fn bootstrap_resolution_names_the_address_it_could_not_use(
724 bootstrap: &str,
725 expected: &str,
726 ) {
727 let options = ConnectionOptions::default();
728 let error = resolve_bootstrap(bootstrap, &options).await.unwrap_err();
729 check!(
730 error.to_string().contains(expected),
731 "{bootstrap:?}: {error}"
732 );
733 }
734
735 #[tokio::test]
736 async fn bootstrap_resolution_keeps_valid_addresses_in_order() {
737 let options = ConnectionOptions::default();
738 let resolved = resolve_bootstrap(" 127.0.0.1:9092, ,localhost:9093", &options)
739 .await
740 .unwrap();
741 check!(
742 resolved.first() == Some(&("127.0.0.1:9092".parse().unwrap(), "127.0.0.1".to_owned()))
743 );
744 check!(
745 resolved
746 .iter()
747 .skip(1)
748 .all(|(address, host)| address.port() == 9093 && host == "localhost")
749 );
750 }
751
752 #[tokio::test]
753 async fn start_rejects_invalid_policy_before_connecting() {
754 let cfg = KafkaMetadataLogConfig {
755 topic_create_timeout: Time::ZERO,
756 ..KafkaMetadataLogConfig::new("not a socket address")
757 };
758
759 let Err(error) = KafkaMetadataEventLog::start(cfg).await else {
760 panic!("invalid policy must fail before network I/O");
761 };
762 assert!(error.to_string().contains("topic_create_timeout"));
763 }
764
765 #[test]
766 fn keyed_tombstone_record_is_partitioned_and_preserves_the_null_value() {
767 let record = producer_record(
768 "__diskless_wal_index",
769 3,
770 2,
771 Some(Bytes::from_static(b"range")),
772 None,
773 )
774 .unwrap();
775
776 assert!(record.topic == "__diskless_wal_index");
777 assert!(record.partition == Some(2));
778 assert!(record.key.as_deref() == Some(b"range".as_slice()));
779 assert!(record.value.is_none());
780 }
781
782 #[yare::parameterized(
783 negative = { -1 },
784 upper_bound = { 3 },
785 )]
786 fn keyed_record_rejects_out_of_range_partitions(partition: i32) {
787 let error = producer_record("index", 3, partition, None, None).unwrap_err();
788 assert2::assert!(matches!(
789 error,
790 MetadataLogError::PartitionOutOfRange {
791 partition: got,
792 count: 3
793 } if got == partition
794 ));
795 }
796}