Skip to main content

log_store/kafka/
log_store.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use common_base::readable_size::ReadableSize;
20use common_meta::datanode::TopicStatsReporter;
21use common_meta::distributed_time_constants::TOPIC_STATS_REPORT_INTERVAL_SECS;
22use common_telemetry::{debug, info, warn};
23use common_time::util::current_time_millis;
24use common_wal::config::kafka::DatanodeKafkaConfig;
25use dashmap::DashMap;
26use futures::future::try_join_all;
27use futures_util::StreamExt;
28use rskafka::client::partition::OffsetAt;
29use snafu::{OptionExt, ResultExt};
30use store_api::logstore::entry::{
31    Entry, Id as EntryId, MultiplePartEntry, MultiplePartHeader, NaiveEntry,
32};
33use store_api::logstore::provider::{KafkaProvider, Provider};
34use store_api::logstore::{AppendBatchResponse, LogStore, SendableEntryStream};
35use store_api::storage::RegionId;
36
37use crate::error::{self, ConsumeRecordSnafu, Error, GetOffsetSnafu, InvalidProviderSnafu, Result};
38use crate::kafka::client_manager::{ClientManager, ClientManagerRef};
39use crate::kafka::consumer::{ConsumerBuilder, RecordsBuffer, RegionWalRange};
40use crate::kafka::periodic_offset_fetcher::PeriodicOffsetFetcher;
41use crate::kafka::producer::OrderedBatchProducerRef;
42use crate::kafka::util::record::{
43    ESTIMATED_META_SIZE, Record, convert_to_kafka_records, is_record_meta_key, maybe_emit_entry,
44    remaining_entries,
45};
46use crate::metrics;
47
48/// Statistics for a topic.
49#[derive(Debug, Clone, Copy, Default)]
50pub struct TopicStat {
51    /// Latest offset for the topic.
52    ///
53    /// The latest offset is updated in two ways:
54    /// - Automatically when the producer successfully commits data to Kafka
55    /// - Periodically by the [PeriodicOffsetFetcher](crate::kafka::periodic_offset_fetcher::PeriodicOffsetFetcher).
56    ///
57    pub latest_offset: u64,
58    /// Total size in bytes of records appended to the topic.
59    pub record_size: u64,
60    /// Total number of records appended to the topic.
61    pub record_num: u64,
62}
63
64/// A log store backed by Kafka.
65#[derive(Debug)]
66pub struct KafkaLogStore {
67    /// The manager of topic clients.
68    client_manager: ClientManagerRef,
69    /// The max size of a batch.
70    max_batch_bytes: usize,
71    /// The consumer wait timeout.
72    consumer_wait_timeout: Duration,
73    /// Ignore missing entries during read WAL.
74    overwrite_entry_start_id: bool,
75    /// The stats of each topic.
76    ///
77    /// This shared map allows multiple components to access the latest stats
78    /// information without needing to query Kafka directly.
79    topic_stats: Arc<DashMap<Arc<KafkaProvider>, TopicStat>>,
80}
81
82struct PeriodicTopicStatsReporter {
83    topic_stats: Arc<DashMap<Arc<KafkaProvider>, TopicStat>>,
84    last_reported_timestamp_millis: i64,
85    report_interval_millis: i64,
86}
87
88impl PeriodicTopicStatsReporter {
89    fn align_ts(ts: i64, report_interval_millis: i64) -> i64 {
90        (ts / report_interval_millis) * report_interval_millis
91    }
92
93    /// Creates a new [PeriodicTopicStatsReporter].
94    ///
95    /// # Panics
96    ///
97    /// Panics if `report_interval` is zero.
98    fn new(
99        topic_stats: Arc<DashMap<Arc<KafkaProvider>, TopicStat>>,
100        report_interval: Duration,
101    ) -> Self {
102        assert!(!report_interval.is_zero());
103        let report_interval_millis = report_interval.as_millis() as i64;
104        let last_reported_timestamp_millis =
105            Self::align_ts(current_time_millis(), report_interval_millis);
106
107        Self {
108            topic_stats,
109            last_reported_timestamp_millis,
110            report_interval_millis,
111        }
112    }
113}
114
115impl TopicStatsReporter for PeriodicTopicStatsReporter {
116    fn reportable_topics(&mut self) -> Vec<common_meta::datanode::TopicStat> {
117        let now = Self::align_ts(current_time_millis(), self.report_interval_millis);
118        if now < self.last_reported_timestamp_millis + self.report_interval_millis {
119            debug!("Skip reporting topic stats because the interval is not reached");
120            return vec![];
121        }
122
123        self.last_reported_timestamp_millis = now;
124        let mut reportable_topics = Vec::with_capacity(self.topic_stats.len());
125        for e in self.topic_stats.iter() {
126            let topic_stat = e.value();
127            let topic_stat = common_meta::datanode::TopicStat {
128                topic: e.key().topic.clone(),
129                latest_entry_id: topic_stat.latest_offset,
130                record_size: topic_stat.record_size,
131                record_num: topic_stat.record_num,
132            };
133            debug!("Reportable topic: {:?}", topic_stat);
134            reportable_topics.push(topic_stat);
135        }
136        debug!("Reportable {} topics at {}", reportable_topics.len(), now);
137        reportable_topics
138    }
139}
140
141impl KafkaLogStore {
142    /// Tries to create a Kafka log store.
143    pub async fn try_new(config: &DatanodeKafkaConfig) -> Result<Self> {
144        let topic_stats = Arc::new(DashMap::new());
145        let client_manager = Arc::new(ClientManager::try_new(config, topic_stats.clone()).await?);
146        let fetcher = PeriodicOffsetFetcher::new(
147            config.topic_latest_offset_fetch_interval,
148            client_manager.clone(),
149        );
150        fetcher.run().await;
151
152        Ok(Self {
153            client_manager,
154            max_batch_bytes: config.max_batch_bytes.as_bytes() as usize,
155            consumer_wait_timeout: config.consumer_wait_timeout,
156            overwrite_entry_start_id: config.overwrite_entry_start_id,
157            topic_stats,
158        })
159    }
160
161    /// Returns the topic stats.
162    pub fn topic_stats_reporter(&self) -> Box<dyn TopicStatsReporter> {
163        Box::new(PeriodicTopicStatsReporter::new(
164            self.topic_stats.clone(),
165            Duration::from_secs(TOPIC_STATS_REPORT_INTERVAL_SECS),
166        ))
167    }
168}
169
170fn build_entry(
171    data: Vec<u8>,
172    entry_id: EntryId,
173    region_id: RegionId,
174    provider: &Provider,
175    max_data_size: usize,
176) -> Entry {
177    if data.len() <= max_data_size {
178        Entry::Naive(NaiveEntry {
179            provider: provider.clone(),
180            region_id,
181            entry_id,
182            data,
183        })
184    } else {
185        let parts = data
186            .chunks(max_data_size)
187            .map(|s| s.into())
188            .collect::<Vec<_>>();
189        let num_parts = parts.len();
190
191        let mut headers = Vec::with_capacity(num_parts);
192        headers.push(MultiplePartHeader::First);
193        headers.extend((1..num_parts - 1).map(MultiplePartHeader::Middle));
194        headers.push(MultiplePartHeader::Last);
195
196        Entry::MultiplePart(MultiplePartEntry {
197            provider: provider.clone(),
198            region_id,
199            entry_id,
200            headers,
201            parts,
202        })
203    }
204}
205
206#[async_trait::async_trait]
207impl LogStore for KafkaLogStore {
208    type Error = Error;
209
210    /// Creates an [Entry].
211    fn entry(
212        &self,
213        data: Vec<u8>,
214        entry_id: EntryId,
215        region_id: RegionId,
216        provider: &Provider,
217    ) -> Result<Entry> {
218        provider
219            .as_kafka_provider()
220            .with_context(|| InvalidProviderSnafu {
221                expected: KafkaProvider::type_name(),
222                actual: provider.type_name(),
223            })?;
224
225        let max_data_size = self.max_batch_bytes - ESTIMATED_META_SIZE;
226        Ok(build_entry(
227            data,
228            entry_id,
229            region_id,
230            provider,
231            max_data_size,
232        ))
233    }
234
235    /// Appends a batch of entries and returns a response containing a map where the key is a region id
236    /// while the value is the id of the last successfully written entry of the region.
237    async fn append_batch(&self, entries: Vec<Entry>) -> Result<AppendBatchResponse> {
238        metrics::METRIC_KAFKA_APPEND_BATCH_BYTES_TOTAL.inc_by(
239            entries
240                .iter()
241                .map(|entry| entry.estimated_size())
242                .sum::<usize>() as u64,
243        );
244        let _timer = metrics::METRIC_KAFKA_APPEND_BATCH_ELAPSED.start_timer();
245
246        if entries.is_empty() {
247            return Ok(AppendBatchResponse::default());
248        }
249
250        let region_ids = entries
251            .iter()
252            .map(|entry| entry.region_id())
253            .collect::<HashSet<_>>();
254        let mut region_grouped_records: HashMap<RegionId, (OrderedBatchProducerRef, Vec<_>)> =
255            HashMap::with_capacity(region_ids.len());
256        let mut region_to_provider = HashMap::with_capacity(region_ids.len());
257        for entry in entries {
258            let provider = entry.provider().as_kafka_provider().with_context(|| {
259                error::InvalidProviderSnafu {
260                    expected: KafkaProvider::type_name(),
261                    actual: entry.provider().type_name(),
262                }
263            })?;
264            region_to_provider.insert(entry.region_id(), provider.clone());
265            let region_id = entry.region_id();
266            match region_grouped_records.entry(region_id) {
267                std::collections::hash_map::Entry::Occupied(mut slot) => {
268                    slot.get_mut().1.extend(convert_to_kafka_records(entry)?);
269                }
270                std::collections::hash_map::Entry::Vacant(slot) => {
271                    let producer = self
272                        .client_manager
273                        .get_or_insert(provider)
274                        .await?
275                        .producer()
276                        .clone();
277
278                    slot.insert((producer, convert_to_kafka_records(entry)?));
279                }
280            }
281        }
282
283        let mut region_grouped_result_receivers = Vec::with_capacity(region_ids.len());
284        for (region_id, (producer, records)) in region_grouped_records {
285            // Safety: `KafkaLogStore::entry` will ensure that the
286            // `Record`'s `approximate_size` must be less or equal to `max_batch_bytes`.
287            region_grouped_result_receivers.push((region_id, producer.produce(records).await?))
288        }
289
290        let region_grouped_max_offset =
291            try_join_all(region_grouped_result_receivers.into_iter().map(
292                |(region_id, receiver)| async move {
293                    receiver.wait().await.map(|offset| (region_id, offset))
294                },
295            ))
296            .await?;
297        debug!(
298            "Appended batch to Kafka, region_grouped_max_offset: {:?}",
299            region_grouped_max_offset
300        );
301
302        Ok(AppendBatchResponse {
303            last_entry_ids: region_grouped_max_offset.into_iter().collect(),
304        })
305    }
306
307    /// Creates a new `EntryStream` to asynchronously generates `Entry` with entry ids.
308    /// Returns entries belonging to `provider`, starting from `entry_id`.
309    async fn read(
310        &self,
311        provider: &Provider,
312        mut entry_id: EntryId,
313    ) -> Result<SendableEntryStream<'static, Entry, Self::Error>> {
314        let provider = provider
315            .as_kafka_provider()
316            .with_context(|| InvalidProviderSnafu {
317                expected: KafkaProvider::type_name(),
318                actual: provider.type_name(),
319            })?;
320
321        let _timer = metrics::METRIC_KAFKA_READ_ELAPSED.start_timer();
322
323        // Gets the client associated with the topic.
324        let client = self
325            .client_manager
326            .get_or_insert(provider)
327            .await?
328            .client()
329            .clone();
330
331        if self.overwrite_entry_start_id {
332            let start_offset =
333                client
334                    .get_offset(OffsetAt::Earliest)
335                    .await
336                    .context(GetOffsetSnafu {
337                        topic: &provider.topic,
338                    })?;
339
340            if entry_id as i64 <= start_offset {
341                warn!(
342                    "The entry_id: {} is less than start_offset: {}, topic: {}. Overwriting entry_id with start_offset",
343                    entry_id, start_offset, &provider.topic
344                );
345
346                entry_id = start_offset as u64;
347            }
348        }
349
350        // Gets the offset of the latest record in the topic. Actually, it's the latest record of the single partition in the topic.
351        // The read operation terminates when this record is consumed.
352        // Warning: the `get_offset` returns the end offset of the latest record. For our usage, it should be decremented.
353        // See: https://kafka.apache.org/36/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html#endOffsets(java.util.Collection)
354        let end_offset = client
355            .get_offset(OffsetAt::Latest)
356            .await
357            .context(GetOffsetSnafu {
358                topic: &provider.topic,
359            })?;
360        let latest_offset = (end_offset as u64).saturating_sub(1);
361        self.topic_stats
362            .entry(provider.clone())
363            .and_modify(|stat| {
364                stat.latest_offset = stat.latest_offset.max(latest_offset);
365            })
366            .or_insert_with(|| TopicStat {
367                latest_offset,
368                record_size: 0,
369                record_num: 0,
370            });
371
372        let range = entry_id..end_offset as u64;
373        if range.is_empty() {
374            warn!("No new entries in range {:?} of ns {}", range, provider);
375            return Ok(futures_util::stream::empty().boxed());
376        }
377
378        let iterator = RegionWalRange::new(range, self.max_batch_bytes);
379        debug!("Reading entries with {:?} of ns {}", iterator, provider);
380
381        // Safety: Must be ok.
382        let mut stream_consumer = ConsumerBuilder::default()
383            .client(client)
384            // Safety: checked before.
385            .buffer(RecordsBuffer::new(iterator))
386            .max_batch_size(self.max_batch_bytes)
387            .max_wait_ms(self.consumer_wait_timeout.as_millis() as u32)
388            .build()
389            .unwrap();
390
391        // A buffer is used to collect records to construct a complete entry.
392        let mut entry_records: HashMap<RegionId, Vec<Record>> = HashMap::new();
393        let provider = provider.clone();
394        let stream = async_stream::stream!({
395            let now = Instant::now();
396            while let Some(consume_result) = stream_consumer.next().await {
397                // Each next on the stream consumer produces a `RecordAndOffset` and a high watermark offset.
398                // The `RecordAndOffset` contains the record data and its start offset.
399                // The high watermark offset is the offset of the last record plus one.
400                let (record_and_offset, high_watermark) =
401                    consume_result.context(ConsumeRecordSnafu {
402                        topic: &provider.topic,
403                    })?;
404                let (kafka_record, offset) = (record_and_offset.record, record_and_offset.offset);
405
406                debug!(
407                    "Read a record at offset {} for topic {}, high watermark: {}",
408                    offset, provider.topic, high_watermark
409                );
410
411                // Ignores no-op records.
412                if kafka_record.value.is_some() {
413                    let key = kafka_record.key;
414                    match Record::try_from_parts(key.as_deref(), kafka_record.value) {
415                        Ok(record) => {
416                            // Tries to construct an entry from records consumed so far.
417                            if let Some(mut entry) =
418                                maybe_emit_entry(&provider, record, &mut entry_records)?
419                            {
420                                // We don't rely on the EntryId generated by mito2.
421                                // Instead, we use the offset return from Kafka as EntryId.
422                                // Therefore, we MUST overwrite the EntryId with RecordOffset.
423                                entry.set_entry_id(offset as u64);
424                                yield Ok(vec![entry]);
425                            }
426                        }
427                        Err(err) => {
428                            // A key that cannot be decoded is skipped only when the log store
429                            // never wrote it. A key that is a JSON object with a `version`
430                            // field may be a `RecordMeta` written by an incompatible version,
431                            // for example one with a newer `RecordType` variant read after a
432                            // rollback or during a rolling upgrade; skipping it would lose the
433                            // entry once the WAL is pruned past its offset, so it stays fatal.
434                            if key.as_deref().is_some_and(is_record_meta_key) {
435                                yield Err(err);
436                                return;
437                            }
438                            warn!(
439                                "Skipping unparsable record at offset {} of topic {}, reason: {}",
440                                offset, provider.topic, err
441                            );
442                        }
443                    }
444                }
445
446                // The termination check runs for every consumed record, including skipped ones.
447                if check_termination(offset, end_offset) {
448                    if let Some(entries) = remaining_entries(&provider, &mut entry_records) {
449                        yield Ok(entries);
450                    }
451                    break;
452                }
453            }
454
455            metrics::METRIC_KAFKA_READ_BYTES_TOTAL.inc_by(stream_consumer.total_fetched_bytes());
456
457            info!(
458                "Fetched {} bytes from topic: {}, start_entry_id: {}, end_offset: {}, elapsed: {:?}",
459                ReadableSize(stream_consumer.total_fetched_bytes()),
460                stream_consumer.topic(),
461                entry_id,
462                end_offset,
463                now.elapsed()
464            );
465        });
466        Ok(Box::pin(stream))
467    }
468
469    /// Creates a new `Namespace` from the given ref.
470    async fn create_namespace(&self, _provider: &Provider) -> Result<()> {
471        Ok(())
472    }
473
474    /// Deletes an existing `Namespace` specified by the given ref.
475    async fn delete_namespace(&self, _provider: &Provider) -> Result<()> {
476        Ok(())
477    }
478
479    /// Lists all existing namespaces.
480    async fn list_namespaces(&self) -> Result<Vec<Provider>> {
481        Ok(vec![])
482    }
483
484    /// Marks all entries with ids `<=entry_id` of the given `namespace` as obsolete,
485    /// so that the log store can safely delete those entries. This method does not guarantee
486    /// that the obsolete entries are deleted immediately.
487    async fn obsolete(
488        &self,
489        _provider: &Provider,
490        _region_id: RegionId,
491        _entry_id: EntryId,
492    ) -> Result<()> {
493        // Kafka WAL pruning is coordinated by metasrv.
494        Ok(())
495    }
496
497    async fn obsolete_all(&self, _provider: &Provider, _region_id: RegionId) -> Result<()> {
498        Ok(())
499    }
500
501    /// Returns the highest entry id of the specified topic in remote WAL.
502    fn latest_entry_id(&self, provider: &Provider) -> Result<EntryId> {
503        let provider = provider
504            .as_kafka_provider()
505            .with_context(|| InvalidProviderSnafu {
506                expected: KafkaProvider::type_name(),
507                actual: provider.type_name(),
508            })?;
509
510        let stat = self
511            .topic_stats
512            .get(provider)
513            .as_deref()
514            .copied()
515            .unwrap_or_default();
516
517        Ok(stat.latest_offset)
518    }
519
520    /// Stops components of the logstore.
521    async fn stop(&self) -> Result<()> {
522        Ok(())
523    }
524}
525
526fn check_termination(offset: i64, end_offset: i64) -> bool {
527    // Terminates the stream if the entry with the end offset was read.
528    if offset >= end_offset {
529        debug!("Stream consumer terminates at offset {}", offset);
530        // There must have no records when the stream terminates.
531        true
532    } else {
533        false
534    }
535}
536
537#[cfg(test)]
538mod tests {
539
540    use std::assert_matches;
541    use std::collections::HashMap;
542    use std::sync::Arc;
543    use std::time::Duration;
544
545    use common_base::readable_size::ReadableSize;
546    use common_meta::datanode::TopicStatsReporter;
547    use common_telemetry::info;
548    use common_wal::config::kafka::DatanodeKafkaConfig;
549    use common_wal::config::kafka::common::KafkaConnectionConfig;
550    use common_wal::maybe_skip_kafka_integration_test;
551    use common_wal::test_util::get_kafka_endpoints;
552    use dashmap::DashMap;
553    use futures::TryStreamExt;
554    use rand::Rng;
555    use rand::prelude::SliceRandom;
556    use rskafka::client::partition::{Compression, OffsetAt};
557    use rskafka::record::Record as KafkaRecord;
558    use store_api::logstore::LogStore;
559    use store_api::logstore::entry::{Entry, MultiplePartEntry, MultiplePartHeader, NaiveEntry};
560    use store_api::logstore::provider::Provider;
561    use store_api::storage::RegionId;
562
563    use super::build_entry;
564    use crate::error;
565    use crate::kafka::log_store::{KafkaLogStore, PeriodicTopicStatsReporter, TopicStat};
566    use crate::kafka::util::record::{ESTIMATED_META_SIZE, convert_to_kafka_records};
567
568    #[test]
569    fn test_build_naive_entry() {
570        let provider = Provider::kafka_provider("my_topic".to_string());
571        let region_id = RegionId::new(1, 1);
572        let entry = build_entry(vec![1; 100], 1, region_id, &provider, 120);
573
574        assert_eq!(
575            entry.into_naive_entry().unwrap(),
576            NaiveEntry {
577                provider,
578                region_id,
579                entry_id: 1,
580                data: vec![1; 100]
581            }
582        )
583    }
584
585    #[test]
586    fn test_build_into_multiple_part_entry() {
587        let provider = Provider::kafka_provider("my_topic".to_string());
588        let region_id = RegionId::new(1, 1);
589        let entry = build_entry(vec![1; 100], 1, region_id, &provider, 50);
590
591        assert_eq!(
592            entry.into_multiple_part_entry().unwrap(),
593            MultiplePartEntry {
594                provider: provider.clone(),
595                region_id,
596                entry_id: 1,
597                headers: vec![MultiplePartHeader::First, MultiplePartHeader::Last],
598                parts: vec![vec![1; 50], vec![1; 50]],
599            }
600        );
601
602        let region_id = RegionId::new(1, 1);
603        let entry = build_entry(vec![1; 100], 1, region_id, &provider, 21);
604
605        assert_eq!(
606            entry.into_multiple_part_entry().unwrap(),
607            MultiplePartEntry {
608                provider,
609                region_id,
610                entry_id: 1,
611                headers: vec![
612                    MultiplePartHeader::First,
613                    MultiplePartHeader::Middle(1),
614                    MultiplePartHeader::Middle(2),
615                    MultiplePartHeader::Middle(3),
616                    MultiplePartHeader::Last
617                ],
618                parts: vec![
619                    vec![1; 21],
620                    vec![1; 21],
621                    vec![1; 21],
622                    vec![1; 21],
623                    vec![1; 16]
624                ],
625            }
626        )
627    }
628
629    fn generate_entries(
630        logstore: &KafkaLogStore,
631        provider: &Provider,
632        num_entries: usize,
633        region_id: RegionId,
634        data_len: usize,
635    ) -> Vec<Entry> {
636        (0..num_entries)
637            .map(|_| {
638                let data: Vec<u8> = (0..data_len).map(|_| rand::random::<u8>()).collect();
639                // Always set `entry_id` to 0, the real entry_id will be set during the read.
640                logstore.entry(data, 0, region_id, provider).unwrap()
641            })
642            .collect()
643    }
644
645    async fn prepare_topic(logstore: &KafkaLogStore, topic_name: &str) {
646        let controller_client = logstore.client_manager.controller_client();
647        controller_client
648            .create_topic(topic_name.to_string(), 1, 1, 5000)
649            .await
650            .unwrap();
651    }
652
653    async fn new_test_logstore(max_batch_bytes: ReadableSize) -> KafkaLogStore {
654        let config = DatanodeKafkaConfig {
655            connection: KafkaConnectionConfig {
656                broker_endpoints: get_kafka_endpoints(),
657                ..Default::default()
658            },
659            max_batch_bytes,
660            ..Default::default()
661        };
662        KafkaLogStore::try_new(&config).await.unwrap()
663    }
664
665    async fn produce_records(
666        logstore: &KafkaLogStore,
667        provider: &Provider,
668        records: Vec<KafkaRecord>,
669    ) {
670        // A freshly created topic can be missing from the broker's metadata for a short
671        // while; `try_create_client` already retries that case.
672        let client = logstore
673            .client_manager
674            .get_or_insert(provider.as_kafka_provider().unwrap())
675            .await
676            .unwrap()
677            .client()
678            .clone();
679        client
680            .produce(records, Compression::NoCompression)
681            .await
682            .unwrap();
683    }
684
685    /// Builds a record that was not written by this log store.
686    fn foreign_record(key: Option<&[u8]>) -> KafkaRecord {
687        KafkaRecord {
688            key: key.map(|key| key.to_vec()),
689            value: Some(b"foreign payload".to_vec()),
690            timestamp: chrono::Utc::now(),
691            headers: Default::default(),
692        }
693    }
694
695    /// Returns the log end offset of the topic, i.e. the exclusive end of the range that
696    /// `read` consumes.
697    async fn log_end_offset(logstore: &KafkaLogStore, provider: &Provider) -> i64 {
698        let client = logstore
699            .client_manager
700            .get_or_insert(provider.as_kafka_provider().unwrap())
701            .await
702            .unwrap();
703        client.client().get_offset(OffsetAt::Latest).await.unwrap()
704    }
705
706    /// Returns one record per kind of key that the log store never writes: a missing key, a
707    /// key that is not JSON, a JSON value that is not an object, and a JSON object without a
708    /// `version` field.
709    fn foreign_records() -> Vec<KafkaRecord> {
710        vec![
711            foreign_record(None),
712            foreign_record(Some(b"not-a-json-key")),
713            foreign_record(Some(b"[1, 2, 3]")),
714            foreign_record(Some(br#"{"hello": "world"}"#)),
715        ]
716    }
717
718    #[tokio::test]
719    async fn test_append_batch_basic() {
720        common_telemetry::init_default_ut_logging();
721        maybe_skip_kafka_integration_test!();
722        let logstore = new_test_logstore(ReadableSize::kb(32)).await;
723        let topic_name = uuid::Uuid::new_v4().to_string();
724        prepare_topic(&logstore, &topic_name).await;
725        let provider = Provider::kafka_provider(topic_name);
726
727        let region_entries = (0..5)
728            .map(|i| {
729                let region_id = RegionId::new(1, i);
730                (
731                    region_id,
732                    generate_entries(&logstore, &provider, 20, region_id, 1024),
733                )
734            })
735            .collect::<HashMap<RegionId, Vec<_>>>();
736
737        let mut all_entries = region_entries
738            .values()
739            .flatten()
740            .cloned()
741            .collect::<Vec<_>>();
742        all_entries.shuffle(&mut rand::rng());
743
744        let response = logstore.append_batch(all_entries.clone()).await.unwrap();
745        // 5 region
746        assert_eq!(response.last_entry_ids.len(), 5);
747        let got_entries = logstore
748            .read(&provider, 0)
749            .await
750            .unwrap()
751            .try_collect::<Vec<_>>()
752            .await
753            .unwrap()
754            .into_iter()
755            .flatten()
756            .collect::<Vec<_>>();
757        for (region_id, _) in region_entries {
758            let expected_entries = all_entries
759                .iter()
760                .filter(|entry| entry.region_id() == region_id)
761                .cloned()
762                .collect::<Vec<_>>();
763            let mut actual_entries = got_entries
764                .iter()
765                .filter(|entry| entry.region_id() == region_id)
766                .cloned()
767                .collect::<Vec<_>>();
768            actual_entries
769                .iter_mut()
770                .for_each(|entry| entry.set_entry_id(0));
771            assert_eq!(expected_entries, actual_entries);
772        }
773        let latest_entry_id = logstore.latest_entry_id(&provider).unwrap();
774        let client = logstore
775            .client_manager
776            .get_or_insert(provider.as_kafka_provider().unwrap())
777            .await
778            .unwrap();
779        assert_eq!(latest_entry_id, 99);
780        // The latest offset is the offset of the last record plus one.
781        let latest = client.client().get_offset(OffsetAt::Latest).await.unwrap();
782        assert_eq!(latest, 100);
783    }
784
785    #[tokio::test]
786    async fn test_append_batch_basic_large() {
787        common_telemetry::init_default_ut_logging();
788        maybe_skip_kafka_integration_test!();
789        let data_size_kb = rand::rng().random_range(9..31usize);
790        info!("Entry size: {}Ki", data_size_kb);
791        let logstore = new_test_logstore(ReadableSize::kb(8)).await;
792        let topic_name = uuid::Uuid::new_v4().to_string();
793        prepare_topic(&logstore, &topic_name).await;
794        let provider = Provider::kafka_provider(topic_name);
795        let region_entries = (0..5)
796            .map(|i| {
797                let region_id = RegionId::new(1, i);
798                (
799                    region_id,
800                    generate_entries(&logstore, &provider, 20, region_id, data_size_kb * 1024),
801                )
802            })
803            .collect::<HashMap<RegionId, Vec<_>>>();
804
805        let mut all_entries = region_entries
806            .values()
807            .flatten()
808            .cloned()
809            .collect::<Vec<_>>();
810        assert_matches!(all_entries[0], Entry::MultiplePart(_));
811        all_entries.shuffle(&mut rand::rng());
812
813        let response = logstore.append_batch(all_entries.clone()).await.unwrap();
814        // 5 region
815        assert_eq!(response.last_entry_ids.len(), 5);
816        let got_entries = logstore
817            .read(&provider, 0)
818            .await
819            .unwrap()
820            .try_collect::<Vec<_>>()
821            .await
822            .unwrap()
823            .into_iter()
824            .flatten()
825            .collect::<Vec<_>>();
826        for (region_id, _) in region_entries {
827            let expected_entries = all_entries
828                .iter()
829                .filter(|entry| entry.region_id() == region_id)
830                .cloned()
831                .collect::<Vec<_>>();
832            let mut actual_entries = got_entries
833                .iter()
834                .filter(|entry| entry.region_id() == region_id)
835                .cloned()
836                .collect::<Vec<_>>();
837            actual_entries
838                .iter_mut()
839                .for_each(|entry| entry.set_entry_id(0));
840            assert_eq!(expected_entries, actual_entries);
841        }
842        let high_wathermark = logstore.latest_entry_id(&provider).unwrap();
843        assert_eq!(high_wathermark, (data_size_kb as u64 / 8 + 1) * 20 * 5 - 1);
844    }
845
846    #[tokio::test]
847    async fn test_read_skips_unparsable_record_inside_multipart_entry() {
848        maybe_skip_kafka_integration_test!();
849        let logstore = new_test_logstore(ReadableSize::kb(8)).await;
850        let topic_name = uuid::Uuid::new_v4().to_string();
851        prepare_topic(&logstore, &topic_name).await;
852        let provider = Provider::kafka_provider(topic_name);
853
854        let region_id = RegionId::new(1, 1);
855        // Splits the entry into First, Middle(1) and Last.
856        let max_data_size = 8 * 1024 - ESTIMATED_META_SIZE;
857        let data = vec![7u8; max_data_size * 2 + 10];
858        let entry = logstore
859            .entry(data.clone(), 0, region_id, &provider)
860            .unwrap();
861        assert_matches!(entry, Entry::MultiplePart(_));
862        let mut records = convert_to_kafka_records(entry).unwrap();
863        assert_eq!(records.len(), 3);
864
865        let last = records.pop().unwrap();
866        // Places an unparsable record before the entry and between every pair of parts.
867        produce_records(
868            &logstore,
869            &provider,
870            vec![foreign_record(Some(b"not-a-json-key"))],
871        )
872        .await;
873        produce_records(&logstore, &provider, vec![records[0].clone()]).await;
874        produce_records(
875            &logstore,
876            &provider,
877            vec![foreign_record(Some(br#"{"hello": "world"}"#))],
878        )
879        .await;
880        produce_records(&logstore, &provider, vec![records[1].clone()]).await;
881        produce_records(
882            &logstore,
883            &provider,
884            vec![foreign_record(Some(b"not-a-json-key"))],
885        )
886        .await;
887        produce_records(&logstore, &provider, vec![last]).await;
888
889        let entries = logstore
890            .read(&provider, 0)
891            .await
892            .unwrap()
893            .try_collect::<Vec<_>>()
894            .await
895            .unwrap()
896            .into_iter()
897            .flatten()
898            .collect::<Vec<_>>();
899        assert_eq!(entries.len(), 1);
900        let entry = entries
901            .into_iter()
902            .next()
903            .unwrap()
904            .into_multiple_part_entry()
905            .unwrap();
906        assert_eq!(
907            entry.headers,
908            vec![
909                MultiplePartHeader::First,
910                MultiplePartHeader::Middle(1),
911                MultiplePartHeader::Last
912            ]
913        );
914        assert_eq!(entry.parts.concat(), data);
915    }
916
917    #[tokio::test]
918    async fn test_read_skips_unparsable_record_across_regions() {
919        maybe_skip_kafka_integration_test!();
920        let logstore = new_test_logstore(ReadableSize::kb(8)).await;
921        let topic_name = uuid::Uuid::new_v4().to_string();
922        prepare_topic(&logstore, &topic_name).await;
923        let provider = Provider::kafka_provider(topic_name);
924
925        let region_a = RegionId::new(1, 1);
926        let region_b = RegionId::new(1, 2);
927        let max_data_size = 8 * 1024 - ESTIMATED_META_SIZE;
928        let data_a = vec![1u8; max_data_size * 2 + 10];
929        let data_b = vec![2u8; max_data_size * 2 + 10];
930        let records_a = convert_to_kafka_records(
931            logstore
932                .entry(data_a.clone(), 0, region_a, &provider)
933                .unwrap(),
934        )
935        .unwrap();
936        let records_b = convert_to_kafka_records(
937            logstore
938                .entry(data_b.clone(), 0, region_b, &provider)
939                .unwrap(),
940        )
941        .unwrap();
942        assert_eq!(records_a.len(), 3);
943        assert_eq!(records_b.len(), 3);
944
945        // Interleaves the parts of both entries with unparsable records.
946        for i in 0..3 {
947            produce_records(&logstore, &provider, vec![records_a[i].clone()]).await;
948            produce_records(&logstore, &provider, vec![records_b[i].clone()]).await;
949            produce_records(
950                &logstore,
951                &provider,
952                vec![foreign_record(Some(b"not-a-json-key"))],
953            )
954            .await;
955        }
956
957        let entries = logstore
958            .read(&provider, 0)
959            .await
960            .unwrap()
961            .try_collect::<Vec<_>>()
962            .await
963            .unwrap()
964            .into_iter()
965            .flatten()
966            .collect::<Vec<_>>();
967        assert_eq!(entries.len(), 2);
968
969        let mut entries = entries
970            .into_iter()
971            .map(|entry| (entry.region_id(), entry.into_multiple_part_entry().unwrap()))
972            .collect::<HashMap<_, _>>();
973        assert_eq!(entries.remove(&region_a).unwrap().parts.concat(), data_a);
974        assert_eq!(entries.remove(&region_b).unwrap().parts.concat(), data_b);
975        assert!(entries.is_empty());
976    }
977
978    #[tokio::test]
979    async fn test_read_finishes_when_last_record_is_unparsable() {
980        maybe_skip_kafka_integration_test!();
981        let logstore = new_test_logstore(ReadableSize::kb(32)).await;
982        let topic_name = uuid::Uuid::new_v4().to_string();
983        prepare_topic(&logstore, &topic_name).await;
984        let provider = Provider::kafka_provider(topic_name);
985
986        let region_id = RegionId::new(1, 1);
987        let mut expected = generate_entries(&logstore, &provider, 3, region_id, 128);
988        logstore.append_batch(expected.clone()).await.unwrap();
989        // Appends an unparsable record as the last record of the read range.
990        produce_records(
991            &logstore,
992            &provider,
993            vec![foreign_record(Some(b"not-a-json-key"))],
994        )
995        .await;
996
997        let entries = tokio::time::timeout(
998            Duration::from_secs(30),
999            logstore
1000                .read(&provider, 0)
1001                .await
1002                .unwrap()
1003                .try_collect::<Vec<_>>(),
1004        )
1005        .await
1006        .expect("read should finish after skipping the unparsable record")
1007        .unwrap()
1008        .into_iter()
1009        .flatten()
1010        .collect::<Vec<_>>();
1011
1012        expected.iter_mut().for_each(|entry| entry.set_entry_id(0));
1013        let mut entries = entries;
1014        entries.iter_mut().for_each(|entry| entry.set_entry_id(0));
1015        assert_eq!(entries, expected);
1016    }
1017
1018    #[tokio::test]
1019    async fn test_read_skips_foreign_records_of_every_kind() {
1020        maybe_skip_kafka_integration_test!();
1021        let logstore = new_test_logstore(ReadableSize::kb(32)).await;
1022        let topic_name = uuid::Uuid::new_v4().to_string();
1023        prepare_topic(&logstore, &topic_name).await;
1024        let provider = Provider::kafka_provider(topic_name);
1025
1026        // Places every kind of unparsable record before the first entry.
1027        for record in foreign_records() {
1028            produce_records(&logstore, &provider, vec![record]).await;
1029        }
1030
1031        let region_id = RegionId::new(1, 1);
1032        let mut expected = generate_entries(&logstore, &provider, 1, region_id, 128);
1033        logstore.append_batch(expected.clone()).await.unwrap();
1034
1035        let entries = logstore
1036            .read(&provider, 0)
1037            .await
1038            .unwrap()
1039            .try_collect::<Vec<_>>()
1040            .await
1041            .unwrap()
1042            .into_iter()
1043            .flatten()
1044            .collect::<Vec<_>>();
1045
1046        expected.iter_mut().for_each(|entry| entry.set_entry_id(0));
1047        let mut entries = entries;
1048        entries.iter_mut().for_each(|entry| entry.set_entry_id(0));
1049        assert_eq!(entries, expected);
1050    }
1051
1052    #[tokio::test]
1053    async fn test_read_finishes_when_topic_only_has_foreign_records() {
1054        maybe_skip_kafka_integration_test!();
1055        let logstore = new_test_logstore(ReadableSize::kb(32)).await;
1056        let topic_name = uuid::Uuid::new_v4().to_string();
1057        prepare_topic(&logstore, &topic_name).await;
1058        let provider = Provider::kafka_provider(topic_name);
1059
1060        for record in foreign_records() {
1061            produce_records(&logstore, &provider, vec![record]).await;
1062        }
1063
1064        // Asserts the read range covers the produced records; an empty range would let `read`
1065        // return without exercising the skip path.
1066        assert_eq!(log_end_offset(&logstore, &provider).await, 4);
1067
1068        let entries = tokio::time::timeout(
1069            Duration::from_secs(30),
1070            logstore
1071                .read(&provider, 0)
1072                .await
1073                .unwrap()
1074                .try_collect::<Vec<_>>(),
1075        )
1076        .await
1077        .expect("read should finish after skipping all unparsable records")
1078        .unwrap();
1079
1080        assert!(entries.into_iter().flatten().next().is_none());
1081    }
1082
1083    #[tokio::test]
1084    async fn test_read_fails_on_incompatible_record_meta() {
1085        maybe_skip_kafka_integration_test!();
1086        let logstore = new_test_logstore(ReadableSize::kb(32)).await;
1087
1088        // Keys that look like a `RecordMeta` but cannot be decoded by this version. They may
1089        // come from an incompatible version of the log store, so the read must not skip them.
1090        let keys: [&[u8]; 2] = [
1091            // A `RecordType` variant this version does not know.
1092            br#"{"version": 0, "tp": "Compressed", "entry_id": 0, "ns": {"region_id": 1, "topic": ""}}"#,
1093            // A field with the wrong type.
1094            br#"{"version": "0", "tp": "Full", "entry_id": 0, "ns": {"region_id": 1, "topic": ""}}"#,
1095        ];
1096        for key in keys {
1097            let topic_name = uuid::Uuid::new_v4().to_string();
1098            prepare_topic(&logstore, &topic_name).await;
1099            let provider = Provider::kafka_provider(topic_name);
1100            produce_records(&logstore, &provider, vec![foreign_record(Some(key))]).await;
1101
1102            let err = logstore
1103                .read(&provider, 0)
1104                .await
1105                .unwrap()
1106                .try_collect::<Vec<Vec<Entry>>>()
1107                .await
1108                .unwrap_err();
1109            assert_matches!(err, error::Error::DecodeJson { .. });
1110        }
1111    }
1112
1113    #[tokio::test]
1114    async fn test_read_fails_on_missing_middle_part() {
1115        maybe_skip_kafka_integration_test!();
1116        let logstore = new_test_logstore(ReadableSize::kb(8)).await;
1117        let topic_name = uuid::Uuid::new_v4().to_string();
1118        prepare_topic(&logstore, &topic_name).await;
1119        let provider = Provider::kafka_provider(topic_name);
1120
1121        let region_id = RegionId::new(1, 1);
1122        // Splits the entry into First, Middle(1), Middle(2) and Last.
1123        let max_data_size = 8 * 1024 - ESTIMATED_META_SIZE;
1124        let data = vec![7u8; max_data_size * 3 + 10];
1125        let entry = logstore.entry(data, 0, region_id, &provider).unwrap();
1126        let records = convert_to_kafka_records(entry).unwrap();
1127        assert_eq!(records.len(), 4);
1128
1129        // Only a missing part of an entry we wrote is fatal; a foreign record is skipped
1130        // because it cannot be one of our parts.
1131        produce_records(
1132            &logstore,
1133            &provider,
1134            vec![foreign_record(Some(b"not-a-json-key"))],
1135        )
1136        .await;
1137        produce_records(&logstore, &provider, vec![records[0].clone()]).await;
1138        produce_records(&logstore, &provider, vec![records[2].clone()]).await;
1139
1140        let err = logstore
1141            .read(&provider, 0)
1142            .await
1143            .unwrap()
1144            .try_collect::<Vec<Vec<Entry>>>()
1145            .await
1146            .unwrap_err();
1147        assert_matches!(err, error::Error::IllegalSequence { .. });
1148    }
1149
1150    #[tokio::test]
1151    async fn test_read_succeeds_on_two_part_entry() {
1152        maybe_skip_kafka_integration_test!();
1153        let logstore = new_test_logstore(ReadableSize::kb(8)).await;
1154        let topic_name = uuid::Uuid::new_v4().to_string();
1155        prepare_topic(&logstore, &topic_name).await;
1156        let provider = Provider::kafka_provider(topic_name);
1157
1158        let region_id = RegionId::new(1, 1);
1159        // Splits the entry into First and Last, without any middle part.
1160        let max_data_size = 8 * 1024 - ESTIMATED_META_SIZE;
1161        let data = vec![7u8; max_data_size + 10];
1162        let entry = logstore
1163            .entry(data.clone(), 0, region_id, &provider)
1164            .unwrap();
1165        let records = convert_to_kafka_records(entry).unwrap();
1166        assert_eq!(records.len(), 2);
1167
1168        for record in records {
1169            produce_records(&logstore, &provider, vec![record]).await;
1170        }
1171
1172        let entries = logstore
1173            .read(&provider, 0)
1174            .await
1175            .unwrap()
1176            .try_collect::<Vec<_>>()
1177            .await
1178            .unwrap()
1179            .into_iter()
1180            .flatten()
1181            .collect::<Vec<_>>();
1182        assert_eq!(entries.len(), 1);
1183        let entry = entries
1184            .into_iter()
1185            .next()
1186            .unwrap()
1187            .into_multiple_part_entry()
1188            .unwrap();
1189        assert_eq!(
1190            entry.headers,
1191            vec![MultiplePartHeader::First, MultiplePartHeader::Last]
1192        );
1193        assert_eq!(entry.parts.concat(), data);
1194    }
1195
1196    #[tokio::test]
1197    async fn test_topic_stats_reporter() {
1198        common_telemetry::init_default_ut_logging();
1199        let topic_stats = Arc::new(DashMap::new());
1200        let provider = Provider::kafka_provider("my_topic".to_string());
1201        topic_stats.insert(
1202            provider.as_kafka_provider().unwrap().clone(),
1203            TopicStat {
1204                latest_offset: 0,
1205                record_size: 0,
1206                record_num: 0,
1207            },
1208        );
1209        let mut reporter = PeriodicTopicStatsReporter::new(topic_stats, Duration::from_secs(1));
1210        // The first reportable topics should be empty.
1211        let reportable_topics = reporter.reportable_topics();
1212        assert_eq!(reportable_topics.len(), 0);
1213
1214        // After 1 second, the reportable topics should be the topic in the topic_stats.
1215        tokio::time::sleep(Duration::from_secs(1)).await;
1216        let reportable_topics = reporter.reportable_topics();
1217        assert_eq!(reportable_topics.len(), 1);
1218
1219        // Call it immediately, should be empty.
1220        let reportable_topics = reporter.reportable_topics();
1221        assert_eq!(reportable_topics.len(), 0);
1222    }
1223}