Skip to main content

log_store/kafka/
consumer.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
15mod range;
16
17pub(crate) use range::RegionWalRange;
18
19use std::collections::VecDeque;
20use std::ops::Range;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::task::{Context, Poll};
24
25use common_telemetry::debug;
26use derive_builder::Builder;
27use futures::future::{BoxFuture, Fuse, FusedFuture};
28use futures::{FutureExt, Stream};
29use pin_project::pin_project;
30use rskafka::client::partition::PartitionClient;
31use rskafka::record::RecordAndOffset;
32
33use crate::kafka::consumer::range::NextBatchHint;
34
35pub struct FetchResult {
36    /// The offsets of the fetched records.
37    pub records: Vec<RecordAndOffset>,
38
39    /// The high watermark of the partition.
40    pub high_watermark: i64,
41
42    /// The size of the response encoded in bytes.
43    pub encoded_response_size: usize,
44}
45
46#[async_trait::async_trait]
47pub trait FetchClient: std::fmt::Debug + Send + Sync {
48    /// Fetch records.
49    ///
50    /// Arguments are identical to [`PartitionClient::fetch_records`].
51    async fn fetch_records(
52        &self,
53        offset: i64,
54        bytes: Range<i32>,
55        max_wait_ms: i32,
56    ) -> rskafka::client::error::Result<FetchResult>;
57
58    fn topic(&self) -> &str;
59}
60
61#[async_trait::async_trait]
62impl FetchClient for PartitionClient {
63    async fn fetch_records(
64        &self,
65        offset: i64,
66        bytes: Range<i32>,
67        max_wait_ms: i32,
68    ) -> rskafka::client::error::Result<FetchResult> {
69        self.fetch_records(offset, bytes, max_wait_ms)
70            .await
71            .map(|r| FetchResult {
72                records: r.records,
73                high_watermark: r.high_watermark,
74                encoded_response_size: r.encoded_response_size,
75            })
76    }
77
78    fn topic(&self) -> &str {
79        self.topic()
80    }
81}
82
83struct FetchResultInner {
84    records_and_offsets: Vec<RecordAndOffset>,
85    batch_size: usize,
86    fetch_bytes: usize,
87    watermark: i64,
88    used_offset: i64,
89}
90
91const MAX_BATCH_SIZE: usize = 52428800;
92const AVG_RECORD_SIZE: usize = 256 * 1024;
93
94/// The [`Consumer`] struct represents a Kafka consumer that fetches messages from
95/// a Kafka cluster. Yielding records respecting the [`RegionWalRange`].
96#[pin_project]
97#[derive(Builder)]
98#[builder(pattern = "owned")]
99pub struct Consumer {
100    #[builder(default = "-1")]
101    last_high_watermark: i64,
102
103    /// The client is used to fetch records from kafka topic.
104    client: Arc<dyn FetchClient>,
105
106    /// The max batch size in a single fetch request.
107    #[builder(default = "MAX_BATCH_SIZE")]
108    max_batch_size: usize,
109
110    /// The max wait milliseconds.
111    #[builder(default = "500")]
112    max_wait_ms: u32,
113
114    /// The avg record size
115    #[builder(default = "AVG_RECORD_SIZE")]
116    avg_record_size: usize,
117
118    /// Termination flag
119    #[builder(default = "false")]
120    terminated: bool,
121
122    /// The buffer of records.
123    buffer: RecordsBuffer,
124
125    /// The fetch future.
126    #[builder(default = "Fuse::terminated()")]
127    fetch_fut: Fuse<BoxFuture<'static, rskafka::client::error::Result<FetchResultInner>>>,
128
129    /// Total fetched bytes.
130    #[builder(default = "0")]
131    total_fetched_bytes: u64,
132}
133
134impl Consumer {
135    /// Returns the total fetched bytes.
136    pub fn total_fetched_bytes(&self) -> u64 {
137        self.total_fetched_bytes
138    }
139
140    /// Returns the topic name.
141    pub fn topic(&self) -> &str {
142        self.client.topic()
143    }
144}
145
146pub(crate) struct RecordsBuffer {
147    buffer: VecDeque<RecordAndOffset>,
148
149    index: RegionWalRange,
150}
151
152impl RecordsBuffer {
153    /// Creates an empty [`RecordsBuffer`]
154    pub fn new(index: RegionWalRange) -> Self {
155        RecordsBuffer {
156            buffer: VecDeque::new(),
157            index,
158        }
159    }
160}
161
162impl RecordsBuffer {
163    fn pop_front(&mut self) -> Option<RecordAndOffset> {
164        while let Some(index) = self.index.peek() {
165            let record_and_offset = self.buffer.pop_front()?;
166            if index == record_and_offset.offset as u64 {
167                self.index.next();
168                return Some(record_and_offset);
169            }
170        }
171
172        self.buffer.clear();
173        None
174    }
175
176    fn extend(&mut self, records: Vec<RecordAndOffset>) {
177        if let (Some(first), Some(index)) = (records.first(), self.index.peek()) {
178            // TODO(weny): throw an error?
179            assert!(
180                index <= first.offset as u64,
181                "index: {index}, first offset: {}",
182                first.offset
183            );
184        }
185        self.buffer.extend(records);
186    }
187}
188
189impl Stream for Consumer {
190    type Item = rskafka::client::error::Result<(RecordAndOffset, i64)>;
191
192    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
193        let this = self.project();
194
195        loop {
196            if *this.terminated {
197                return Poll::Ready(None);
198            }
199
200            if this.buffer.index.peek().is_none() {
201                return Poll::Ready(None);
202            }
203
204            if let Some(x) = this.buffer.pop_front() {
205                debug!("Yielding record with offset: {}", x.offset);
206                return Poll::Ready(Some(Ok((x, *this.last_high_watermark))));
207            }
208
209            if this.fetch_fut.is_terminated() {
210                match this.buffer.index.peek() {
211                    Some(next_offset) => {
212                        let client = Arc::clone(this.client);
213                        let max_wait_ms = *this.max_wait_ms as i32;
214                        let offset = next_offset as i64;
215                        let NextBatchHint { bytes, len } = this
216                            .buffer
217                            .index
218                            .next_batch_hint(*this.avg_record_size)
219                            .unwrap_or(NextBatchHint {
220                                bytes: *this.avg_record_size,
221                                len: 1,
222                            });
223
224                        let fetch_range =
225                            1i32..(bytes.saturating_add(1).min(*this.max_batch_size) as i32);
226                        *this.fetch_fut = FutureExt::fuse(Box::pin(async move {
227                            let FetchResult {
228                                records: records_and_offsets,
229                                high_watermark: watermark,
230                                encoded_response_size,
231                                ..
232                            } = client
233                                .fetch_records(offset, fetch_range, max_wait_ms)
234                                .await?;
235
236                            Ok(FetchResultInner {
237                                records_and_offsets,
238                                watermark,
239                                used_offset: offset,
240                                fetch_bytes: encoded_response_size,
241                                batch_size: len,
242                            })
243                        }));
244                    }
245                    None => {
246                        return Poll::Ready(None);
247                    }
248                }
249            }
250
251            let data = futures::ready!(this.fetch_fut.poll_unpin(cx));
252
253            match data {
254                Ok(FetchResultInner {
255                    mut records_and_offsets,
256                    watermark,
257                    used_offset,
258                    fetch_bytes,
259                    batch_size,
260                }) => {
261                    // Sort records by offset in case they aren't in order
262                    records_and_offsets.sort_unstable_by_key(|x| x.offset);
263                    *this.last_high_watermark = watermark;
264                    if !records_and_offsets.is_empty() {
265                        *this.avg_record_size = fetch_bytes / records_and_offsets.len();
266                        debug!("set avg_record_size: {}", *this.avg_record_size);
267                    }
268                    *this.total_fetched_bytes += fetch_bytes as u64;
269
270                    debug!(
271                        "Fetch result: {:?}, used_offset: {used_offset}, max_batch_size: {fetch_bytes}, expected batch_num: {batch_size}, actual batch_num: {}",
272                        records_and_offsets
273                            .iter()
274                            .map(|record| record.offset)
275                            .collect::<Vec<_>>(),
276                        records_and_offsets.len()
277                    );
278                    this.buffer.extend(records_and_offsets);
279                    continue;
280                }
281                Err(e) => {
282                    *this.terminated = true;
283
284                    return Poll::Ready(Some(Err(e)));
285                }
286            }
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use std::collections::VecDeque;
294    use std::ops::Range;
295    use std::sync::Arc;
296
297    use chrono::{TimeZone, Utc};
298    use futures::TryStreamExt;
299    use futures::future::Fuse;
300    use rskafka::record::{Record, RecordAndOffset};
301
302    use super::*;
303    use crate::kafka::consumer::{Consumer, RecordsBuffer};
304
305    #[derive(Debug)]
306    struct MockFetchClient {
307        record: Record,
308    }
309
310    #[async_trait::async_trait]
311    impl FetchClient for MockFetchClient {
312        async fn fetch_records(
313            &self,
314            offset: i64,
315            bytes: Range<i32>,
316            _max_wait_ms: i32,
317        ) -> rskafka::client::error::Result<FetchResult> {
318            let record_size = self.record.approximate_size();
319            let num = (bytes.end.unsigned_abs() as usize / record_size).max(1);
320
321            let records = (0..num)
322                .map(|idx| RecordAndOffset {
323                    record: self.record.clone(),
324                    offset: offset + idx as i64,
325                })
326                .collect::<Vec<_>>();
327
328            let max_offset = offset + records.len() as i64;
329            let encoded_response_size = records.iter().map(|r| r.record.approximate_size()).sum();
330            Ok(FetchResult {
331                records,
332                high_watermark: max_offset,
333                encoded_response_size,
334            })
335        }
336
337        fn topic(&self) -> &str {
338            "test"
339        }
340    }
341
342    fn test_record() -> Record {
343        Record {
344            key: Some(vec![0; 4]),
345            value: Some(vec![0; 6]),
346            headers: Default::default(),
347            timestamp: Utc.timestamp_millis_opt(1337).unwrap(),
348        }
349    }
350
351    #[tokio::test]
352    async fn test_consumer_without_index() {
353        common_telemetry::init_default_ut_logging();
354        let record = test_record();
355        let mock_client = MockFetchClient {
356            record: record.clone(),
357        };
358        let index = RegionWalRange::new(0..30, 1024);
359        let consumer = Consumer {
360            last_high_watermark: -1,
361            client: Arc::new(mock_client),
362            max_batch_size: usize::MAX,
363            max_wait_ms: 500,
364            avg_record_size: record.approximate_size(),
365            terminated: false,
366            buffer: RecordsBuffer {
367                buffer: VecDeque::new(),
368                index,
369            },
370            fetch_fut: Fuse::terminated(),
371            total_fetched_bytes: 0,
372        };
373
374        let records = consumer.try_collect::<Vec<_>>().await.unwrap();
375        assert_eq!(
376            records
377                .into_iter()
378                .map(|(x, _)| x.offset)
379                .collect::<Vec<_>>(),
380            (0..30).collect::<Vec<_>>()
381        )
382    }
383
384    #[tokio::test]
385    async fn test_consumer_range_boundaries() {
386        for (start, end, max_batch_size) in [
387            (0, 0, 1024),
388            (5, 5, 1024),
389            (6, 5, 1024),
390            (5, 6, 1024),
391            (5, 35, 128),
392        ] {
393            let record = test_record();
394            let consumer = ConsumerBuilder::default()
395                .client(Arc::new(MockFetchClient {
396                    record: record.clone(),
397                }))
398                .buffer(RecordsBuffer::new(RegionWalRange::new(
399                    start..end,
400                    max_batch_size,
401                )))
402                .avg_record_size(record.approximate_size())
403                .build()
404                .unwrap();
405            let offsets = consumer
406                .try_collect::<Vec<_>>()
407                .await
408                .unwrap()
409                .into_iter()
410                .map(|(record, _)| record.offset as u64)
411                .collect::<Vec<_>>();
412            assert_eq!(offsets, (start..end).collect::<Vec<_>>());
413        }
414    }
415}