1use std::{
4 pin::Pin,
5 task::{Context, Poll},
6 time::Duration,
7};
8
9use futures_core::Stream;
10use futures_util::{StreamExt, stream};
11use s2_common::{
12 caps::RECORD_BATCH_MAX,
13 read_extent::CountOrBytes,
14 record::{Metered, MeteredSize},
15};
16use tokio::time::Instant;
17
18use crate::types::{AppendInput, AppendRecord, AppendRecordBatch, FencingToken, ValidationError};
19
20const RECORD_BATCH_MIN: CountOrBytes = CountOrBytes { count: 1, bytes: 8 };
21
22#[derive(Debug, Clone)]
23pub struct BatchLimits {
25 max_batch_bytes: usize,
26 max_batch_records: usize,
27}
28
29impl Default for BatchLimits {
30 fn default() -> Self {
31 Self {
32 max_batch_bytes: RECORD_BATCH_MAX.bytes,
33 max_batch_records: RECORD_BATCH_MAX.count,
34 }
35 }
36}
37
38impl BatchLimits {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn with_max_batch_bytes(self, max_batch_bytes: usize) -> Result<Self, ValidationError> {
50 if max_batch_bytes < RECORD_BATCH_MIN.bytes {
51 return Err(ValidationError(format!(
52 "max_batch_bytes ({max_batch_bytes}) must be at least {}",
53 RECORD_BATCH_MIN.bytes
54 )));
55 }
56 if max_batch_bytes > RECORD_BATCH_MAX.bytes {
57 return Err(ValidationError(format!(
58 "max_batch_bytes ({max_batch_bytes}) must not exceed {}",
59 RECORD_BATCH_MAX.bytes
60 )));
61 }
62 Ok(Self {
63 max_batch_bytes,
64 ..self
65 })
66 }
67
68 pub fn with_max_batch_records(self, max_batch_records: usize) -> Result<Self, ValidationError> {
74 if max_batch_records < RECORD_BATCH_MIN.count {
75 return Err(ValidationError(format!(
76 "max_batch_records ({max_batch_records}) must be at least {}",
77 RECORD_BATCH_MIN.count
78 )));
79 }
80 if max_batch_records > RECORD_BATCH_MAX.count {
81 return Err(ValidationError(format!(
82 "max_batch_records ({max_batch_records}) must not exceed {}",
83 RECORD_BATCH_MAX.count
84 )));
85 }
86 Ok(Self {
87 max_batch_records,
88 ..self
89 })
90 }
91}
92
93#[derive(Debug, Clone)]
94pub struct BatchingConfig {
96 linger: Duration,
97 limits: BatchLimits,
98}
99
100impl Default for BatchingConfig {
101 fn default() -> Self {
102 Self {
103 linger: Duration::from_millis(5),
104 limits: BatchLimits::default(),
105 }
106 }
107}
108
109impl BatchingConfig {
110 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn with_linger(self, linger: Duration) -> Self {
119 Self { linger, ..self }
120 }
121
122 pub fn with_limits(self, limits: BatchLimits) -> Self {
124 Self { limits, ..self }
125 }
126}
127
128pub struct AppendInputs {
130 pub(crate) batches: AppendRecordBatches,
131 pub(crate) fencing_token: Option<FencingToken>,
132 pub(crate) match_seq_num: Option<u64>,
133}
134
135impl AppendInputs {
136 pub fn new(batches: AppendRecordBatches) -> Self {
138 Self {
139 batches,
140 fencing_token: None,
141 match_seq_num: None,
142 }
143 }
144
145 pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
147 Self {
148 fencing_token: Some(fencing_token),
149 ..self
150 }
151 }
152
153 pub fn with_match_seq_num(self, seq_num: u64) -> Self {
156 Self {
157 match_seq_num: Some(seq_num),
158 ..self
159 }
160 }
161}
162
163impl Stream for AppendInputs {
164 type Item = Result<AppendInput, ValidationError>;
165
166 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
167 match self.batches.poll_next_unpin(cx) {
168 Poll::Ready(Some(Ok(batch))) => {
169 let match_seq_num = self.match_seq_num;
170 if let Some(seq_num) = self.match_seq_num.as_mut() {
171 *seq_num += batch.len() as u64;
172 }
173 Poll::Ready(Some(Ok(AppendInput {
174 records: batch,
175 match_seq_num,
176 fencing_token: self.fencing_token.clone(),
177 })))
178 }
179 Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
180 Poll::Ready(None) => Poll::Ready(None),
181 Poll::Pending => Poll::Pending,
182 }
183 }
184}
185
186pub struct AppendRecordBatches {
188 inner: Pin<Box<dyn Stream<Item = Result<AppendRecordBatch, ValidationError>> + Send>>,
189}
190
191impl AppendRecordBatches {
192 pub fn from_stream(
194 records: impl Stream<Item = impl Into<AppendRecord> + Send> + Send + Unpin + 'static,
195 config: BatchingConfig,
196 ) -> Self {
197 Self {
198 inner: Box::pin(append_record_batches(records, config)),
199 }
200 }
201
202 pub fn from_iter(
216 records: impl IntoIterator<Item = impl Into<AppendRecord>>,
217 limits: BatchLimits,
218 ) -> Result<Self, ValidationError> {
219 let mut batches = Vec::new();
220 let mut batch = Metered::with_capacity(limits.max_batch_records);
221
222 for item in records {
223 let record = Metered::from(item.into());
224 if record.metered_size() > limits.max_batch_bytes {
225 return Err(ValidationError(format!(
226 "record size in metered bytes ({}) exceeds max_batch_bytes ({})",
227 record.metered_size(),
228 limits.max_batch_bytes
229 )));
230 }
231
232 if !batch.is_empty() && would_overflow_batch(&limits, &batch, &record) {
233 batches.push(AppendRecordBatch::from(std::mem::replace(
234 &mut batch,
235 Metered::with_capacity(limits.max_batch_records),
236 )));
237 }
238
239 batch.push(record);
240 if is_batch_full(&limits, &batch) {
241 batches.push(AppendRecordBatch::from(std::mem::replace(
242 &mut batch,
243 Metered::with_capacity(limits.max_batch_records),
244 )));
245 }
246 }
247
248 if !batch.is_empty() {
249 batches.push(batch.into());
250 }
251
252 Ok(Self {
253 inner: Box::pin(stream::iter(batches.into_iter().map(Ok))),
254 })
255 }
256}
257
258impl Stream for AppendRecordBatches {
259 type Item = Result<AppendRecordBatch, ValidationError>;
260
261 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
262 self.inner.as_mut().poll_next(cx)
263 }
264}
265
266fn is_batch_full(limits: &BatchLimits, batch: &Metered<Vec<AppendRecord>>) -> bool {
267 batch.len() >= limits.max_batch_records || batch.metered_size() >= limits.max_batch_bytes
268}
269
270fn would_overflow_batch(
271 limits: &BatchLimits,
272 batch: &Metered<Vec<AppendRecord>>,
273 record: &Metered<AppendRecord>,
274) -> bool {
275 batch.len() + 1 > limits.max_batch_records
276 || batch.metered_size() + record.metered_size() > limits.max_batch_bytes
277}
278
279fn append_record_batches(
280 mut records: impl Stream<Item = impl Into<AppendRecord> + Send> + Send + Unpin + 'static,
281 config: BatchingConfig,
282) -> impl Stream<Item = Result<AppendRecordBatch, ValidationError>> + Send + 'static {
283 async_stream::try_stream! {
284 let mut batch = Metered::with_capacity(config.limits.max_batch_records);
285 let mut overflowed_record: Option<Metered<AppendRecord>> = None;
286
287 let linger_deadline = tokio::time::sleep(config.linger);
288 tokio::pin!(linger_deadline);
289
290 'outer: loop {
291 let first_record = match overflowed_record.take() {
292 Some(pair) => pair,
293 None => match records.next().await {
294 Some(item) => Metered::from(item.into()),
295 None => break,
296 },
297 };
298
299 if first_record.metered_size() > config.limits.max_batch_bytes {
300 Err(ValidationError(format!(
301 "record size in metered bytes ({}) exceeds max_batch_bytes ({})",
302 first_record.metered_size(),
303 config.limits.max_batch_bytes
304 )))?;
305 }
306 batch.push(first_record);
307
308 while !is_batch_full(&config.limits, &batch) && overflowed_record.is_none() {
309 if batch.len() == 1 {
310 linger_deadline
311 .as_mut()
312 .reset(Instant::now() + config.linger);
313 }
314
315 tokio::select! {
316 next_record = records.next() => {
317 match next_record {
318 Some(record) => {
319 let record = Metered::from(record.into());
320 if would_overflow_batch(&config.limits, &batch, &record) {
321 overflowed_record = Some(record);
322 } else {
323 batch.push(record);
324 }
325 }
326 None => {
327 yield AppendRecordBatch::from(std::mem::replace(
328 &mut batch,
329 Metered::with_capacity(config.limits.max_batch_records),
330 ));
331 break 'outer;
332 }
333 }
334 },
335 _ = &mut linger_deadline, if !batch.is_empty() => {
336 break;
337 }
338 };
339 }
340
341 yield AppendRecordBatch::from(std::mem::replace(
342 &mut batch,
343 Metered::with_capacity(config.limits.max_batch_records),
344 ));
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use assert_matches::assert_matches;
352 use futures_util::TryStreamExt;
353
354 use super::*;
355 use crate::types::MeteredBytes as _;
356
357 #[tokio::test]
358 async fn batches_should_be_empty_when_record_stream_is_empty() {
359 let batches: Vec<_> = AppendRecordBatches::from_stream(
360 futures_util::stream::iter::<Vec<AppendRecord>>(vec![]),
361 BatchingConfig::default(),
362 )
363 .collect()
364 .await;
365 assert_eq!(batches.len(), 0);
366 }
367
368 #[tokio::test]
369 async fn batches_respect_count_limit() -> Result<(), ValidationError> {
370 let records: Vec<_> = (0..10)
371 .map(|i| AppendRecord::new(format!("record{i}")))
372 .collect::<Result<_, _>>()?;
373 let config = BatchingConfig::default()
374 .with_limits(BatchLimits::default().with_max_batch_records(3)?);
375 let batches: Vec<_> =
376 AppendRecordBatches::from_stream(futures_util::stream::iter(records), config)
377 .try_collect()
378 .await?;
379
380 assert_eq!(batches.len(), 4);
381 assert_eq!(batches[0].len(), 3);
382 assert_eq!(batches[1].len(), 3);
383 assert_eq!(batches[2].len(), 3);
384 assert_eq!(batches[3].len(), 1);
385
386 Ok(())
387 }
388
389 #[tokio::test]
390 async fn batches_respect_bytes_limit() -> Result<(), ValidationError> {
391 let records: Vec<_> = (0..10)
392 .map(|i| AppendRecord::new(format!("record{i}")))
393 .collect::<Result<_, _>>()?;
394 let single_record_bytes = records[0].metered_bytes();
395 let max_batch_bytes = single_record_bytes * 3;
396
397 let config = BatchingConfig::default()
398 .with_limits(BatchLimits::default().with_max_batch_bytes(max_batch_bytes)?);
399 let batches: Vec<_> =
400 AppendRecordBatches::from_stream(futures_util::stream::iter(records), config)
401 .try_collect()
402 .await?;
403
404 assert_eq!(batches.len(), 4);
405 assert_eq!(batches[0].metered_bytes(), max_batch_bytes);
406 assert_eq!(batches[1].metered_bytes(), max_batch_bytes);
407 assert_eq!(batches[2].metered_bytes(), max_batch_bytes);
408 assert_eq!(batches[3].metered_bytes(), single_record_bytes);
409
410 Ok(())
411 }
412
413 #[tokio::test(start_paused = true)]
414 async fn batches_flush_after_linger_when_stream_remains_open() -> Result<(), ValidationError> {
415 let records: Vec<_> = (0..2)
416 .map(|i| AppendRecord::new(format!("record{i}")))
417 .collect::<Result<_, _>>()?;
418 let records = futures_util::stream::iter(records).chain(futures_util::stream::pending());
419 let config = BatchingConfig::default().with_linger(Duration::from_millis(5));
420 let mut batches = AppendRecordBatches::from_stream(records, config);
421 let next_batch = tokio::spawn(async move { batches.next().await });
422
423 tokio::task::yield_now().await;
424 tokio::time::advance(Duration::from_millis(6)).await;
425
426 let batch = next_batch.await.unwrap().unwrap()?;
427 assert_eq!(batch.len(), 2);
428 Ok(())
429 }
430
431 #[tokio::test]
432 async fn batching_should_error_when_it_sees_oversized_record() -> Result<(), ValidationError> {
433 let record = AppendRecord::new("hello-world")?;
434 let record_bytes = record.metered_bytes();
435 let max_batch_bytes = 10;
436
437 let config = BatchingConfig::default()
438 .with_limits(BatchLimits::default().with_max_batch_bytes(max_batch_bytes)?);
439 let results: Vec<_> =
440 AppendRecordBatches::from_stream(futures_util::stream::iter(vec![record]), config)
441 .collect()
442 .await;
443
444 assert_eq!(results.len(), 1);
445 assert_matches!(&results[0], Err(err) => {
446 assert_eq!(
447 err.to_string(),
448 format!("record size in metered bytes ({record_bytes}) exceeds max_batch_bytes ({max_batch_bytes})")
449 );
450 });
451
452 Ok(())
453 }
454
455 #[tokio::test]
456 async fn eager_batches_should_be_empty_when_record_iter_is_empty() {
457 let batches =
458 AppendRecordBatches::from_iter(std::iter::empty::<AppendRecord>(), BatchLimits::new())
459 .unwrap()
460 .try_collect::<Vec<_>>()
461 .await
462 .unwrap();
463 assert!(batches.is_empty());
464 }
465
466 #[tokio::test]
467 async fn eager_batches_respect_count_limit() -> Result<(), ValidationError> {
468 let records: Vec<_> = (0..10)
469 .map(|i| AppendRecord::new(format!("record{i}")))
470 .collect::<Result<_, _>>()?;
471 let limits = BatchLimits::new().with_max_batch_records(3)?;
472 let batches = AppendRecordBatches::from_iter(records, limits)?
473 .try_collect::<Vec<_>>()
474 .await?;
475
476 assert_eq!(batches.len(), 4);
477 assert_eq!(batches[0].len(), 3);
478 assert_eq!(batches[1].len(), 3);
479 assert_eq!(batches[2].len(), 3);
480 assert_eq!(batches[3].len(), 1);
481 Ok(())
482 }
483
484 #[tokio::test]
485 async fn eager_batches_respect_bytes_limit() -> Result<(), ValidationError> {
486 let records: Vec<_> = (0..10)
487 .map(|i| AppendRecord::new(format!("record{i}")))
488 .collect::<Result<_, _>>()?;
489 let single_record_bytes = records[0].metered_bytes();
490 let max_batch_bytes = single_record_bytes * 3;
491 let limits = BatchLimits::new().with_max_batch_bytes(max_batch_bytes)?;
492 let batches = AppendRecordBatches::from_iter(records, limits)?
493 .try_collect::<Vec<_>>()
494 .await?;
495
496 assert_eq!(batches.len(), 4);
497 assert_eq!(batches[0].metered_bytes(), max_batch_bytes);
498 assert_eq!(batches[1].metered_bytes(), max_batch_bytes);
499 assert_eq!(batches[2].metered_bytes(), max_batch_bytes);
500 assert_eq!(batches[3].metered_bytes(), single_record_bytes);
501 Ok(())
502 }
503
504 #[tokio::test]
505 async fn eager_batching_should_error_when_record_is_oversized() -> Result<(), ValidationError> {
506 let record = AppendRecord::new("hello-world")?;
507 let record_bytes = record.metered_bytes();
508 let max_batch_bytes = 10;
509 let limits = BatchLimits::new().with_max_batch_bytes(max_batch_bytes)?;
510 let err = AppendRecordBatches::from_iter([record], limits)
511 .err()
512 .unwrap();
513
514 assert_eq!(
515 err.to_string(),
516 format!(
517 "record size in metered bytes ({record_bytes}) exceeds max_batch_bytes ({max_batch_bytes})"
518 )
519 );
520 Ok(())
521 }
522}