1use 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#[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 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}