Skip to main content

nisshi_storage/service/
fetch.rs

1// Copyright ⓒ 2024-2026 Peter Morgan <peter.james.morgan@gmail.com>
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::{cmp::min, time::SystemTime};
16
17use nisshi_sans_io::{
18    ApiKey, ErrorCode, FetchRequest, FetchResponse, IsolationLevel,
19    fetch_request::{FetchPartition, FetchTopic},
20    fetch_response::{
21        EpochEndOffset, FetchableTopicResponse, LeaderIdAndEpoch, PartitionData, SnapshotId,
22    },
23    metadata_response::MetadataResponseTopic,
24    record::deflated::{Batch, Frame},
25};
26use rama::{Context, Service};
27use tokio::time::{Duration, Instant, sleep};
28use tracing::{debug, error, instrument};
29
30use crate::{Error, Result, Storage, Topition};
31
32/// A [`Service`] using [`Storage`] as [`Context`] taking [`FetchRequest`] returning [`FetchResponse`].
33/// ```
34/// use rama::{Context, Layer as _, Service as _, layer::MapStateLayer};
35/// use nisshi_sans_io::{
36///     CreateTopicsRequest, ErrorCode, FetchRequest,
37///     create_topics_request::CreatableTopic,
38///     fetch_request::{FetchPartition, FetchTopic},
39/// };
40/// use nisshi_storage::{CreateTopicsService, Error, FetchService, StorageContainer};
41/// use url::Url;
42///
43/// # #[tokio::main]
44/// # async fn main() -> Result<(), Error> {
45/// const CLUSTER_ID: &str = "nisshi";
46/// const NODE_ID: i32 = 111;
47/// const HOST: &str = "localhost";
48/// const PORT: i32 = 9092;
49///
50/// let storage = StorageContainer::builder()
51///     .cluster_id(CLUSTER_ID)
52///     .node_id(NODE_ID)
53///     .advertised_listener(Url::parse(&format!("tcp://{HOST}:{PORT}"))?)
54///     .storage(Url::parse("memory://nisshi/")?)
55///     .build()
56///     .await?;
57///
58/// let create_topic = {
59///     let storage = storage.clone();
60///     MapStateLayer::new(|_| storage).into_layer(CreateTopicsService)
61/// };
62///
63/// let name = "abcba";
64///
65/// let response = create_topic
66///     .serve(
67///         Context::default(),
68///         CreateTopicsRequest::default()
69///             .topics(Some(vec![
70///                 CreatableTopic::default()
71///                     .name(name.into())
72///                     .num_partitions(5)
73///                     .replication_factor(3)
74///                     .assignments(Some([].into()))
75///                     .configs(Some([].into())),
76///             ]))
77///             .validate_only(Some(false)),
78///     )
79///     .await?;
80///
81/// let topics = response.topics.unwrap_or_default();
82/// assert_eq!(1, topics.len());
83/// assert_eq!(ErrorCode::None, ErrorCode::try_from(topics[0].error_code)?);
84///
85/// let fetch = {
86///     let storage = storage.clone();
87///     MapStateLayer::new(|_| storage).into_layer(FetchService)
88/// };
89///
90/// let partition = 0;
91///
92/// let response = fetch
93///     .serve(
94///         Context::default(),
95///         FetchRequest::default()
96///             .topics(Some(
97///                 [FetchTopic::default()
98///                     .topic(Some(name.into()))
99///                     .partitions(Some(
100///                         [FetchPartition::default().partition(partition)].into(),
101///                     ))]
102///                 .into(),
103///             ))
104///             .max_bytes(Some(0))
105///             .max_wait_ms(5_000),
106///     )
107///     .await?;
108///
109/// let topics = response.responses.as_deref().unwrap_or_default();
110/// assert_eq!(1, topics.len());
111/// let partitions = topics[0].partitions.as_deref().unwrap_or_default();
112/// assert_eq!(1, partitions.len());
113/// assert_eq!(
114///     ErrorCode::None,
115///     ErrorCode::try_from(partitions[0].error_code)?
116/// );
117/// # Ok(())
118/// # }
119/// ```
120#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
121pub struct FetchService;
122
123impl ApiKey for FetchService {
124    const KEY: i16 = FetchRequest::KEY;
125}
126
127impl FetchService {
128    #[allow(clippy::too_many_arguments)]
129    #[instrument(skip(self,ctx,min_bytes,isolation,fetch_partition), fields(partition = fetch_partition.partition))]
130    async fn fetch_partition<G>(
131        &self,
132        ctx: &Context<G>,
133        max_wait: Duration,
134        min_bytes: u32,
135        max_bytes: &mut u32,
136        isolation: IsolationLevel,
137        topic: &str,
138        fetch_partition: &FetchPartition,
139    ) -> Result<PartitionData>
140    where
141        G: Storage,
142    {
143        let started_at = Instant::now();
144
145        let partition_index = fetch_partition.partition;
146        let tp = Topition::new(topic, partition_index);
147
148        let mut batches = Vec::new();
149
150        let mut offset = fetch_partition.fetch_offset;
151
152        loop {
153            if *max_bytes == 0 {
154                break;
155            }
156
157            debug!(offset);
158
159            let mut fetched = ctx
160                .state()
161                .fetch(
162                    &tp,
163                    offset,
164                    min_bytes,
165                    *max_bytes,
166                    isolation,
167                    max_wait.saturating_sub(started_at.elapsed()),
168                )
169                .await
170                .inspect(|r| debug!(?tp, ?offset, ?r))
171                .inspect_err(|error| error!(?tp, ?error))?;
172
173            *max_bytes =
174                u32::try_from(fetched.byte_size()).map(|bytes| max_bytes.saturating_sub(bytes))?;
175
176            debug!(?offset, ?fetched, max_bytes);
177
178            if fetched.is_empty() || fetched.first().is_some_and(|batch| batch.record_count == 0) {
179                break;
180            }
181
182            if let Some(latest) = fetched
183                .iter()
184                .map(|batch| batch.base_offset + batch.record_count as i64)
185                .max()
186                .inspect(|latest| debug!(latest))
187            {
188                offset = latest;
189            }
190
191            batches.append(&mut fetched);
192        }
193
194        let offset_stage = ctx
195            .state()
196            .offset_stage(&tp)
197            .await
198            .inspect_err(|error| error!(?error, ?tp))?;
199
200        Ok(PartitionData::default()
201            .partition_index(partition_index)
202            .error_code(ErrorCode::None.into())
203            .high_watermark(offset_stage.high_watermark())
204            .last_stable_offset(Some(offset_stage.last_stable()))
205            .log_start_offset(Some(offset_stage.log_start()))
206            .diverging_epoch(None)
207            .current_leader(None)
208            .snapshot_id(None)
209            .aborted_transactions(Some([].into()))
210            .preferred_read_replica(Some(-1))
211            .records(if batches.is_empty() {
212                None
213            } else {
214                Some(Frame { batches })
215            }))
216        .inspect(|r| debug!(?r, elapsed = ?started_at.elapsed()))
217    }
218
219    fn unknown_topic_response(&self, fetch: &FetchTopic) -> Result<FetchableTopicResponse> {
220        Ok(FetchableTopicResponse::default()
221            .topic(fetch.topic.clone())
222            .topic_id(Some([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
223            .partitions(fetch.partitions.as_ref().map(|partitions| {
224                partitions
225                    .iter()
226                    .map(|partition| {
227                        PartitionData::default()
228                            .partition_index(partition.partition)
229                            .error_code(ErrorCode::UnknownTopicOrPartition.into())
230                            .high_watermark(0)
231                            .last_stable_offset(Some(0))
232                            .log_start_offset(Some(-1))
233                            .diverging_epoch(Some(
234                                EpochEndOffset::default().epoch(-1).end_offset(-1),
235                            ))
236                            .current_leader(Some(
237                                LeaderIdAndEpoch::default().leader_id(0).leader_epoch(0),
238                            ))
239                            .snapshot_id(Some(SnapshotId::default().end_offset(-1).epoch(-1)))
240                            .aborted_transactions(Some([].into()))
241                            .preferred_read_replica(Some(-1))
242                            .records(None)
243                    })
244                    .collect()
245            })))
246    }
247
248    #[allow(clippy::too_many_arguments)]
249    #[instrument(skip(self, ctx, min_bytes, isolation, fetch))]
250    async fn fetch_topic<G>(
251        &self,
252        ctx: &Context<G>,
253        max_wait: Duration,
254        min_bytes: u32,
255        max_bytes: &mut u32,
256        isolation: IsolationLevel,
257        fetch: &FetchTopic,
258        is_first_non_empty: &mut bool,
259    ) -> Result<FetchableTopicResponse>
260    where
261        G: Storage,
262    {
263        let started_at = Instant::now();
264
265        let metadata = ctx.state().metadata(Some(&[fetch.into()])).await?;
266
267        if let Some(MetadataResponseTopic {
268            topic_id,
269            name: Some(name),
270            ..
271        }) = metadata.topics().first()
272        {
273            let mut partitions = Vec::new();
274
275            for fetch_partition in fetch.partitions.as_ref().unwrap_or(&Vec::new()) {
276                let partition_max_bytes = if *is_first_non_empty {
277                    *max_bytes
278                } else {
279                    min(fetch_partition.partition_max_bytes as u32, *max_bytes)
280                };
281
282                let mut partition_bytes = partition_max_bytes;
283                debug!(partition_bytes, is_first_non_empty);
284
285                let remaining = max_wait.saturating_sub(started_at.elapsed());
286
287                let partition = self
288                    .fetch_partition(
289                        ctx,
290                        remaining,
291                        min_bytes,
292                        &mut partition_bytes,
293                        isolation,
294                        name,
295                        fetch_partition,
296                    )
297                    .await?;
298
299                *is_first_non_empty = *is_first_non_empty
300                    && partition
301                        .records
302                        .as_ref()
303                        .is_some_and(|records| records.batches.is_empty());
304
305                debug!(partition_bytes, is_first_non_empty);
306
307                *max_bytes =
308                    max_bytes.saturating_sub(partition_max_bytes.saturating_sub(partition_bytes));
309
310                partitions.push(partition);
311            }
312
313            Ok(FetchableTopicResponse::default()
314                .topic(fetch.topic.to_owned())
315                .topic_id(topic_id.to_owned())
316                .partitions(Some(partitions)))
317        } else {
318            self.unknown_topic_response(fetch)
319        }
320    }
321
322    #[instrument(skip(self, ctx, isolation, topics))]
323    pub(crate) async fn fetch<G>(
324        &self,
325        ctx: &Context<G>,
326        max_wait: Duration,
327        min_bytes: u32,
328        max_bytes: &mut u32,
329        isolation: IsolationLevel,
330        topics: &[FetchTopic],
331    ) -> Result<Vec<FetchableTopicResponse>>
332    where
333        G: Storage,
334    {
335        debug!(?isolation, ?topics);
336
337        if topics.is_empty() {
338            Ok(vec![])
339        } else {
340            let started_at = SystemTime::now();
341            let mut responses = vec![];
342            let mut iteration = 0;
343            let mut bytes = 0;
344            let mut is_first_non_empty = true;
345
346            while !max_wait.saturating_sub(started_at.elapsed()?).is_zero() && bytes <= min_bytes {
347                debug!(?bytes, remaining = ?max_wait.saturating_sub(started_at.elapsed()?));
348
349                responses.clear();
350
351                let fetch_started_at = SystemTime::now();
352                for fetch in topics.iter() {
353                    let fetch_response = self
354                        .fetch_topic(
355                            ctx,
356                            max_wait.saturating_sub(started_at.elapsed()?),
357                            min_bytes,
358                            max_bytes,
359                            isolation,
360                            fetch,
361                            &mut is_first_non_empty,
362                        )
363                        .await?;
364
365                    responses.push(fetch_response);
366                }
367
368                bytes += u32::try_from(responses.byte_size())?;
369
370                let remaining = max_wait.saturating_sub(started_at.elapsed()?);
371
372                debug!(?iteration, ?max_wait, ?remaining, ?bytes, ?min_bytes);
373
374                if bytes > min_bytes {
375                    break;
376                }
377
378                {
379                    let fetch_elapsed = fetch_started_at.elapsed()?;
380
381                    // we have some data to return to the client,
382                    // we haven't met the minimum size requirement,
383                    // but we don't have enough (estimated) time remaining to do another round
384                    if !responses.is_empty() && remaining < fetch_elapsed {
385                        debug!(responses.len = responses.len(), ?remaining, ?fetch_elapsed);
386                        break;
387                    }
388                }
389
390                sleep(remaining / 2).await;
391
392                iteration += 1;
393            }
394
395            Ok(responses)
396        }
397    }
398}
399
400impl<G> Service<G, FetchRequest> for FetchService
401where
402    G: Storage,
403{
404    type Response = FetchResponse;
405    type Error = Error;
406
407    #[instrument(skip(ctx, req))]
408    async fn serve(
409        &self,
410        ctx: Context<G>,
411        req: FetchRequest,
412    ) -> Result<Self::Response, Self::Error> {
413        let started_at = SystemTime::now();
414
415        let responses = Some(if let Some(topics) = req.topics {
416            let isolation_level = req
417                .isolation_level
418                .map_or(Ok(IsolationLevel::ReadUncommitted), |isolation| {
419                    IsolationLevel::try_from(isolation)
420                })?;
421
422            let max_wait_ms = u64::try_from(req.max_wait_ms).map(Duration::from_millis)?;
423
424            let min_bytes = u32::try_from(req.min_bytes)?;
425
426            const DEFAULT_MAX_BYTES: u32 = 5 * 1024 * 1024;
427
428            let mut max_bytes = req.max_bytes.map_or(Ok(DEFAULT_MAX_BYTES), |max_bytes| {
429                u32::try_from(max_bytes).map(|max_bytes| max_bytes.min(DEFAULT_MAX_BYTES))
430            })?;
431
432            self.fetch(
433                &ctx,
434                max_wait_ms,
435                min_bytes,
436                &mut max_bytes,
437                isolation_level,
438                topics.as_ref(),
439            )
440            .await?
441        } else {
442            vec![]
443        });
444
445        Ok(FetchResponse::default()
446            .throttle_time_ms(Some(0))
447            .error_code(Some(ErrorCode::None.into()))
448            .session_id(Some(0))
449            .node_endpoints(Some([].into()))
450            .responses(responses))
451        .inspect(|r| debug!(?r, elapsed = ?started_at.elapsed().ok()))
452    }
453}
454
455trait ByteSize {
456    fn byte_size(&self) -> u64;
457}
458
459impl<T> ByteSize for Vec<T>
460where
461    T: ByteSize,
462{
463    fn byte_size(&self) -> u64 {
464        self.iter().map(|item| item.byte_size()).sum()
465    }
466}
467
468impl<T> ByteSize for Option<T>
469where
470    T: ByteSize,
471{
472    fn byte_size(&self) -> u64 {
473        self.as_ref().map_or(0, |some| some.byte_size())
474    }
475}
476
477impl ByteSize for Batch {
478    fn byte_size(&self) -> u64 {
479        self.record_data.len() as u64
480    }
481}
482
483impl ByteSize for Frame {
484    fn byte_size(&self) -> u64 {
485        self.batches.byte_size()
486    }
487}
488
489impl ByteSize for PartitionData {
490    fn byte_size(&self) -> u64 {
491        self.records.byte_size()
492    }
493}
494
495impl ByteSize for FetchableTopicResponse {
496    fn byte_size(&self) -> u64 {
497        self.partitions.byte_size()
498    }
499}