1use std::{
4 pin::Pin,
5 task::{Context, Poll},
6 time::Duration,
7};
8
9use futures_core::Stream;
10use futures_util::StreamExt;
11use s2_common::{caps::RECORD_BATCH_MAX, read_extent::CountOrBytes};
12use tokio::time::Instant;
13
14use crate::types::{
15 AppendInput, AppendRecord, AppendRecordBatch, FencingToken, MeteredBytes, ValidationError,
16};
17
18const RECORD_BATCH_MIN: CountOrBytes = CountOrBytes { count: 1, bytes: 8 };
19
20#[derive(Debug, Clone)]
21pub struct BatchingConfig {
23 linger: Duration,
24 max_batch_bytes: usize,
25 max_batch_records: usize,
26}
27
28impl Default for BatchingConfig {
29 fn default() -> Self {
30 Self {
31 linger: Duration::from_millis(5),
32 max_batch_bytes: RECORD_BATCH_MAX.bytes,
33 max_batch_records: RECORD_BATCH_MAX.count,
34 }
35 }
36}
37
38impl BatchingConfig {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn with_linger(self, linger: Duration) -> Self {
48 Self { linger, ..self }
49 }
50
51 pub fn with_max_batch_bytes(self, max_batch_bytes: usize) -> Result<Self, ValidationError> {
57 if max_batch_bytes < RECORD_BATCH_MIN.bytes {
58 return Err(ValidationError(format!(
59 "max_batch_bytes ({max_batch_bytes}) must be at least {}",
60 RECORD_BATCH_MIN.bytes
61 )));
62 }
63 if max_batch_bytes > RECORD_BATCH_MAX.bytes {
64 return Err(ValidationError(format!(
65 "max_batch_bytes ({max_batch_bytes}) must not exceed {}",
66 RECORD_BATCH_MAX.bytes
67 )));
68 }
69 Ok(Self {
70 max_batch_bytes,
71 ..self
72 })
73 }
74
75 pub fn with_max_batch_records(self, max_batch_records: usize) -> Result<Self, ValidationError> {
81 if max_batch_records < RECORD_BATCH_MIN.count {
82 return Err(ValidationError(format!(
83 "max_batch_records ({max_batch_records}) must be at least {}",
84 RECORD_BATCH_MIN.count
85 )));
86 }
87 if max_batch_records > RECORD_BATCH_MAX.count {
88 return Err(ValidationError(format!(
89 "max_batch_records ({max_batch_records}) must not exceed {}",
90 RECORD_BATCH_MAX.count
91 )));
92 }
93 Ok(Self {
94 max_batch_records,
95 ..self
96 })
97 }
98}
99
100pub struct AppendInputs {
102 pub(crate) batches: AppendRecordBatches,
103 pub(crate) fencing_token: Option<FencingToken>,
104 pub(crate) match_seq_num: Option<u64>,
105}
106
107impl AppendInputs {
108 pub fn new(
110 records: impl Stream<Item = impl Into<AppendRecord> + Send> + Send + Unpin + 'static,
111 config: BatchingConfig,
112 ) -> Self {
113 Self {
114 batches: AppendRecordBatches::new(records, config),
115 fencing_token: None,
116 match_seq_num: None,
117 }
118 }
119
120 pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
122 Self {
123 fencing_token: Some(fencing_token),
124 ..self
125 }
126 }
127
128 pub fn with_match_seq_num(self, seq_num: u64) -> Self {
131 Self {
132 match_seq_num: Some(seq_num),
133 ..self
134 }
135 }
136}
137
138impl Stream for AppendInputs {
139 type Item = Result<AppendInput, ValidationError>;
140
141 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
142 match self.batches.poll_next_unpin(cx) {
143 Poll::Ready(Some(Ok(batch))) => {
144 let match_seq_num = self.match_seq_num;
145 if let Some(seq_num) = self.match_seq_num.as_mut() {
146 *seq_num += batch.len() as u64;
147 }
148 Poll::Ready(Some(Ok(AppendInput {
149 records: batch,
150 match_seq_num,
151 fencing_token: self.fencing_token.clone(),
152 })))
153 }
154 Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
155 Poll::Ready(None) => Poll::Ready(None),
156 Poll::Pending => Poll::Pending,
157 }
158 }
159}
160
161pub struct AppendRecordBatches {
163 inner: Pin<Box<dyn Stream<Item = Result<AppendRecordBatch, ValidationError>> + Send>>,
164}
165
166impl AppendRecordBatches {
167 pub fn new(
169 records: impl Stream<Item = impl Into<AppendRecord> + Send> + Send + Unpin + 'static,
170 config: BatchingConfig,
171 ) -> Self {
172 Self {
173 inner: Box::pin(append_record_batches(records, config)),
174 }
175 }
176}
177
178impl Stream for AppendRecordBatches {
179 type Item = Result<AppendRecordBatch, ValidationError>;
180
181 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
182 self.inner.as_mut().poll_next(cx)
183 }
184}
185
186fn is_batch_full(config: &BatchingConfig, count: usize, bytes: usize) -> bool {
187 count >= config.max_batch_records || bytes >= config.max_batch_bytes
188}
189
190fn would_overflow_batch(
191 config: &BatchingConfig,
192 count: usize,
193 bytes: usize,
194 record: &AppendRecord,
195) -> bool {
196 count + 1 > config.max_batch_records || bytes + record.metered_bytes() > config.max_batch_bytes
197}
198
199fn append_record_batches(
200 mut records: impl Stream<Item = impl Into<AppendRecord> + Send> + Send + Unpin + 'static,
201 config: BatchingConfig,
202) -> impl Stream<Item = Result<AppendRecordBatch, ValidationError>> + Send + 'static {
203 async_stream::try_stream! {
204 let mut batch = AppendRecordBatch::with_capacity(config.max_batch_records);
205 let mut overflowed_record: Option<AppendRecord> = None;
206
207 let linger_deadline = tokio::time::sleep(config.linger);
208 tokio::pin!(linger_deadline);
209
210 'outer: loop {
211 let first_record = match overflowed_record.take() {
212 Some(record) => record,
213 None => match records.next().await {
214 Some(item) => item.into(),
215 None => break,
216 },
217 };
218
219 let record_bytes = first_record.metered_bytes();
220 if record_bytes > config.max_batch_bytes {
221 Err(ValidationError(format!(
222 "record size in metered bytes ({record_bytes}) exceeds max_batch_bytes ({})",
223 config.max_batch_bytes
224 )))?;
225 }
226 batch.push(first_record);
227
228 while !is_batch_full(&config, batch.len(), batch.metered_bytes())
229 && overflowed_record.is_none()
230 {
231 if batch.len() == 1 {
232 linger_deadline
233 .as_mut()
234 .reset(Instant::now() + config.linger);
235 }
236
237 tokio::select! {
238 next_record = records.next() => {
239 match next_record {
240 Some(record) => {
241 let record: AppendRecord = record.into();
242 if would_overflow_batch(&config, batch.len(), batch.metered_bytes(), &record) {
243 overflowed_record = Some(record);
244 } else {
245 batch.push(record);
246 }
247 }
248 None => {
249 yield std::mem::replace(&mut batch, AppendRecordBatch::with_capacity(config.max_batch_records));
250 break 'outer;
251 }
252 }
253 },
254 _ = &mut linger_deadline, if !batch.is_empty() => {
255 break;
256 }
257 };
258 }
259
260 yield std::mem::replace(
261 &mut batch,
262 AppendRecordBatch::with_capacity(config.max_batch_records),
263 );
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use assert_matches::assert_matches;
271 use futures_util::TryStreamExt;
272
273 use super::*;
274
275 #[tokio::test]
276 async fn batches_should_be_empty_when_record_stream_is_empty() {
277 let batches: Vec<_> = AppendRecordBatches::new(
278 futures_util::stream::iter::<Vec<AppendRecord>>(vec![]),
279 BatchingConfig::default(),
280 )
281 .collect()
282 .await;
283 assert_eq!(batches.len(), 0);
284 }
285
286 #[tokio::test]
287 async fn batches_respect_count_limit() -> Result<(), ValidationError> {
288 let records: Vec<_> = (0..10)
289 .map(|i| AppendRecord::new(format!("record{i}")))
290 .collect::<Result<_, _>>()?;
291 let config = BatchingConfig::default().with_max_batch_records(3)?;
292 let batches: Vec<_> = AppendRecordBatches::new(futures_util::stream::iter(records), config)
293 .try_collect()
294 .await?;
295
296 assert_eq!(batches.len(), 4);
297 assert_eq!(batches[0].len(), 3);
298 assert_eq!(batches[1].len(), 3);
299 assert_eq!(batches[2].len(), 3);
300 assert_eq!(batches[3].len(), 1);
301
302 Ok(())
303 }
304
305 #[tokio::test]
306 async fn batches_respect_bytes_limit() -> Result<(), ValidationError> {
307 let records: Vec<_> = (0..10)
308 .map(|i| AppendRecord::new(format!("record{i}")))
309 .collect::<Result<_, _>>()?;
310 let single_record_bytes = records[0].metered_bytes();
311 let max_batch_bytes = single_record_bytes * 3;
312
313 let config = BatchingConfig::default().with_max_batch_bytes(max_batch_bytes)?;
314 let batches: Vec<_> = AppendRecordBatches::new(futures_util::stream::iter(records), config)
315 .try_collect()
316 .await?;
317
318 assert_eq!(batches.len(), 4);
319 assert_eq!(batches[0].metered_bytes(), max_batch_bytes);
320 assert_eq!(batches[1].metered_bytes(), max_batch_bytes);
321 assert_eq!(batches[2].metered_bytes(), max_batch_bytes);
322 assert_eq!(batches[3].metered_bytes(), single_record_bytes);
323
324 Ok(())
325 }
326
327 #[tokio::test]
328 async fn batching_should_error_when_it_sees_oversized_record() -> Result<(), ValidationError> {
329 let record = AppendRecord::new("hello-world")?;
330 let record_bytes = record.metered_bytes();
331 let max_batch_bytes = 10;
332
333 let config = BatchingConfig::default().with_max_batch_bytes(max_batch_bytes)?;
334 let results: Vec<_> =
335 AppendRecordBatches::new(futures_util::stream::iter(vec![record]), config)
336 .collect()
337 .await;
338
339 assert_eq!(results.len(), 1);
340 assert_matches!(&results[0], Err(err) => {
341 assert_eq!(
342 err.to_string(),
343 format!("record size in metered bytes ({record_bytes}) exceeds max_batch_bytes ({max_batch_bytes})")
344 );
345 });
346
347 Ok(())
348 }
349}