weir_core/batch.rs
1//! Payload codecs for the `PushBatch` / `AckBatch` frame pair — N records in
2//! one round trip, answered once.
3//!
4//! Both are **additive within wire v1**: message-type bytes `0x08`–`0xFF` are
5//! reserved for exactly this, so [`WIRE_VERSION`](crate::WIRE_VERSION) is
6//! unchanged and the thirty frozen conformance vectors stay byte-identical. A
7//! daemon that predates these types answers `Nack(UnknownMessage)`, which is an
8//! actionable, permanent error rather than a misparse.
9//!
10//! # Why the reply is a bitmap and what a clear bit means
11//!
12//! Every sub-record is validated at **ingest**, before any record of the batch
13//! is enqueued. A validation failure — an empty record, one over
14//! `max_payload_bytes`, a framing disagreement — rejects the *whole frame* with
15//! a plain `Nack` carrying its reason, and closes the connection. That is the
16//! existing "permanent protocol error ⇒ close" contract, unchanged.
17//!
18//! The consequence is what makes a bare bitmap sufficient: by the time an
19//! `AckBatch` is sent at all, every possible per-record failure is a *runtime*
20//! one — WAB cap, queue refusal, a write or fsync that did not succeed, an ack
21//! deadline — and every one of those is `InternalError`, which the wire
22//! protocol already defines as the sole transient reason. So a clear bit needs
23//! no reason byte to be actionable:
24//!
25//! > **bit 1 ⇒ the record is durable at the requested tier.**
26//! > **bit 0 ⇒ not durable as of this reply: retry it, and expect that it may
27//! > nonetheless have been written** (at-least-once, exactly as a single-record
28//! > `Nack(InternalError)` already means).
29//!
30//! The asymmetry is deliberate and must be documented wherever this is
31//! described: a set bit is a strong statement inheriting weir's crown
32//! invariant, while a clear bit is a weak one. A reader who treats clear bits as
33//! "definitely not written" will build exactly-once retry on a foundation that
34//! does not support it.
35
36use crate::version::MAX_PAYLOAD_HARD_CAP;
37
38/// Version byte at offset 0 of a `PushBatch` body.
39///
40/// It leads for the same reason [`COORDINATE_VERSION`](crate::COORDINATE_VERSION)
41/// does: the body can then grow *inside* wire v1, because a reader meeting a
42/// version it does not know rejects the frame instead of parsing a prefix of a
43/// layout it has never seen.
44pub const BATCH_VERSION: u8 = 1;
45
46/// Version byte at offset 0 of an `AckBatch` payload.
47pub const ACK_BATCH_VERSION: u8 = 1;
48
49/// `version(1) + record_count(2)` — the fixed prefix of a `PushBatch` body.
50pub const BATCH_HEADER_LEN: usize = 1 + 2;
51
52/// `version(1) + record_count(2)` — the fixed prefix of an `AckBatch` payload,
53/// followed by `ceil(N / 8)` bitmap bytes.
54pub const ACK_BATCH_HEADER_LEN: usize = 1 + 2;
55
56/// Absolute ceiling on records in one `PushBatch`, across all code paths.
57///
58/// Chosen so the `AckBatch` reply stays inside the response bound weir already
59/// has: `3 + ceil(2048 / 8)` = 259 bytes, under the 298-byte
60/// [`MAX_TRACKED_ACK_PAYLOAD_LEN`](crate::MAX_TRACKED_ACK_PAYLOAD_LEN). That
61/// keeps 298 the largest response payload weir ever sends, so no client gains a
62/// new maximum to allocate for and no published size claim changes.
63///
64/// It is a *safety* bound, not a tuning knob. `record_count` is a `u16`, so a
65/// ~320 KiB frame could otherwise declare 65,535 records — 99.998% of the
66/// daemon's entire global queue capacity, all of it targeting the single
67/// partition its connection is pinned to.
68pub const MAX_BATCH_RECORDS_HARD_CAP: usize = 2048;
69
70/// Largest `AckBatch` payload: the fixed prefix plus a bitmap for the hard cap.
71pub const MAX_ACK_BATCH_PAYLOAD_LEN: usize =
72 ACK_BATCH_HEADER_LEN + MAX_BATCH_RECORDS_HARD_CAP.div_ceil(8);
73
74/// Why a `PushBatch` body or `AckBatch` payload could not be decoded.
75///
76/// Variants name the conformance-vector tags, matching
77/// [`CoordinateError`](crate::CoordinateError)'s convention so a non-Rust
78/// implementer can map a rejection to the vector that pins it.
79#[derive(Debug, Clone, PartialEq, Eq)]
80#[non_exhaustive]
81pub enum BatchError {
82 /// Shorter than the fixed prefix every body has.
83 Truncated {
84 /// Bytes present.
85 len: usize,
86 /// Bytes needed.
87 need: usize,
88 },
89 /// The leading version byte is one this build cannot parse.
90 UnsupportedVersion {
91 /// The version byte received.
92 version: u8,
93 },
94 /// `record_count` was zero. An empty batch is not a no-op to be tolerated:
95 /// it is a framing disagreement, because nothing legitimate produces one.
96 EmptyBatch,
97 /// `record_count` exceeds the cap in force.
98 TooManyRecords {
99 /// The declared count.
100 count: usize,
101 /// The cap.
102 cap: usize,
103 },
104 /// A record inside the batch declared a zero length. The WAB cannot
105 /// represent an empty record — a zero `payload_len` is its end-of-records
106 /// sentinel — so this is rejected here rather than deeper.
107 EmptyRecord {
108 /// Index of the offending record.
109 index: usize,
110 },
111 /// A record inside the batch exceeds the per-record cap. Checked per record
112 /// and not only per frame, because the frame cap bounds the *batch* and an
113 /// operator who sets `max_payload_bytes` is bounding a *record*.
114 RecordTooLarge {
115 /// Index of the offending record.
116 index: usize,
117 /// Its declared length.
118 len: usize,
119 /// The per-record cap.
120 cap: usize,
121 },
122 /// An entry's declared length runs past the end of the body.
123 TruncatedRecord {
124 /// Index of the offending record.
125 index: usize,
126 },
127 /// The walk consumed a different number of bytes than the body holds, or
128 /// found a different number of records than `record_count` declared.
129 ///
130 /// Both are the same class of fault: the two ends disagree about the layout.
131 /// Using whichever half seems more plausible is how that disagreement turns
132 /// into records silently dropped or invented.
133 LengthMismatch {
134 /// What the body declared or implied.
135 declared: usize,
136 /// What was actually found.
137 actual: usize,
138 },
139 /// A padding bit in the final bitmap byte was set.
140 ///
141 /// Rejected rather than ignored, following the `ReservedFlagsSet`
142 /// precedent: weir does not silently tolerate a set bit it has no meaning
143 /// for, because the next version might give it one.
144 PaddingNotZero {
145 /// The offending final byte.
146 byte: u8,
147 },
148}
149
150impl core::fmt::Display for BatchError {
151 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152 match self {
153 Self::Truncated { len, need } => {
154 write!(f, "batch body truncated: {len} bytes, need at least {need}")
155 }
156 Self::UnsupportedVersion { version } => {
157 write!(f, "unsupported batch version {version:#04x}")
158 }
159 Self::EmptyBatch => write!(f, "batch declared zero records"),
160 Self::TooManyRecords { count, cap } => {
161 write!(f, "batch declared {count} records, cap is {cap}")
162 }
163 Self::EmptyRecord { index } => {
164 write!(f, "record {index} is zero-length")
165 }
166 Self::RecordTooLarge { index, len, cap } => {
167 write!(f, "record {index} is {len} bytes, cap is {cap}")
168 }
169 Self::TruncatedRecord { index } => {
170 write!(f, "record {index} runs past the end of the body")
171 }
172 Self::LengthMismatch { declared, actual } => {
173 write!(f, "batch declared {declared}, found {actual}")
174 }
175 Self::PaddingNotZero { byte } => {
176 write!(f, "padding bits set in final bitmap byte {byte:#04x}")
177 }
178 }
179 }
180}
181
182impl core::error::Error for BatchError {}
183
184/// Encodes a `PushBatch` body: version, count, then length-prefixed records.
185///
186/// Encoding does not enforce the caps — `decode_batch_body` does, and the
187/// conformance suite needs to build deliberately-oversized bodies to check that
188/// the rejection happens.
189pub fn encode_batch_body(records: &[&[u8]]) -> Vec<u8> {
190 let total: usize = BATCH_HEADER_LEN + records.iter().map(|r| 4 + r.len()).sum::<usize>();
191 let mut out = Vec::with_capacity(total);
192 out.push(BATCH_VERSION);
193 out.extend_from_slice(&(records.len() as u16).to_le_bytes());
194 for r in records {
195 out.extend_from_slice(&(r.len() as u32).to_le_bytes());
196 out.extend_from_slice(r);
197 }
198 out
199}
200
201/// Decodes a `PushBatch` body into borrowed record slices.
202///
203/// # Validation order
204///
205/// Deliberate, and every step happens before any allocation sized by a
206/// peer-chosen number. The frame's payload CRC has already been verified by the
207/// caller, but that proves only the absence of *accidental* corruption: a
208/// hostile peer computes a perfectly valid CRC over a body declaring 65,535
209/// records in three bytes. The CRC contributes nothing to these checks and they
210/// are all mandatory.
211///
212/// 1. the fixed prefix is present
213/// 2. the version is one we know
214/// 3. the count is non-zero
215/// 4. the count is within `max_records` — **before** reserving anything
216/// 5. each entry is non-empty, within `max_record_len`, and inside the body
217/// 6. the walk ends exactly at the end of the body, having found exactly
218/// `record_count` records
219pub fn decode_batch_body(
220 body: &[u8],
221 max_records: usize,
222 max_record_len: usize,
223) -> Result<Vec<&[u8]>, BatchError> {
224 if body.len() < BATCH_HEADER_LEN {
225 return Err(BatchError::Truncated {
226 len: body.len(),
227 need: BATCH_HEADER_LEN,
228 });
229 }
230 let version = body[0];
231 if version != BATCH_VERSION {
232 return Err(BatchError::UnsupportedVersion { version });
233 }
234 let declared = u16::from_le_bytes([body[1], body[2]]) as usize;
235 if declared == 0 {
236 return Err(BatchError::EmptyBatch);
237 }
238 let cap = max_records.min(MAX_BATCH_RECORDS_HARD_CAP);
239 if declared > cap {
240 return Err(BatchError::TooManyRecords {
241 count: declared,
242 cap,
243 });
244 }
245
246 // `declared` is now bounded by a value this daemon chose, so reserving
247 // against it is safe. This is the line the 2.0.3 payload_len fix exists to
248 // prevent the unbounded version of.
249 let mut out: Vec<&[u8]> = Vec::with_capacity(declared);
250 let mut cursor = BATCH_HEADER_LEN;
251 let record_cap = max_record_len.min(MAX_PAYLOAD_HARD_CAP);
252
253 while cursor < body.len() {
254 let index = out.len();
255 if cursor + 4 > body.len() {
256 return Err(BatchError::TruncatedRecord { index });
257 }
258 let len = u32::from_le_bytes([
259 body[cursor],
260 body[cursor + 1],
261 body[cursor + 2],
262 body[cursor + 3],
263 ]) as usize;
264 cursor += 4;
265
266 if len == 0 {
267 return Err(BatchError::EmptyRecord { index });
268 }
269 if len > record_cap {
270 return Err(BatchError::RecordTooLarge {
271 index,
272 len,
273 cap: record_cap,
274 });
275 }
276 if cursor + len > body.len() {
277 return Err(BatchError::TruncatedRecord { index });
278 }
279 // Stop before overrunning the declared count, so a body carrying more
280 // records than it declares is a LengthMismatch rather than a silent
281 // truncation of the extras.
282 if index == declared {
283 return Err(BatchError::LengthMismatch {
284 declared,
285 actual: declared + 1,
286 });
287 }
288 out.push(&body[cursor..cursor + len]);
289 cursor += len;
290 }
291
292 if out.len() != declared {
293 return Err(BatchError::LengthMismatch {
294 declared,
295 actual: out.len(),
296 });
297 }
298 if cursor != body.len() {
299 return Err(BatchError::LengthMismatch {
300 declared: cursor,
301 actual: body.len(),
302 });
303 }
304 Ok(out)
305}
306
307/// Bit `i` of the bitmap, **LSB-first within each byte**.
308///
309/// The order is stated once, here, and every other site refers to it. It is the
310/// single most dangerous under-specification in this codec: a reader using the
311/// opposite convention produces a *well-formed* frame whose meaning is inverted
312/// — header CRC passes, payload CRC passes, `payload_len` is correct — and the
313/// result is a producer told a record is durable when the daemon said it failed.
314/// That is a false ack, the one outcome weir exists to prevent, and no checksum
315/// or cap detects it.
316///
317/// LSB-first is chosen because it is the idiom of every language weir has a
318/// client in: Rust and C bit arithmetic, Go's `math/bits`, Java's `BitSet`.
319#[inline]
320#[must_use]
321pub const fn bitmap_bit(index: usize) -> (usize, u8) {
322 (index / 8, 1u8 << (index % 8))
323}
324
325/// Encodes an `AckBatch` payload from per-record outcomes.
326///
327/// `accepted[i] == true` means record `i` is durable at the requested tier.
328/// Padding bits in the final byte are left zero, which the decoder requires.
329pub fn encode_ack_batch(accepted: &[bool]) -> Vec<u8> {
330 let n = accepted.len();
331 let mut out = Vec::with_capacity(ACK_BATCH_HEADER_LEN + n.div_ceil(8));
332 out.push(ACK_BATCH_VERSION);
333 out.extend_from_slice(&(n as u16).to_le_bytes());
334 out.resize(ACK_BATCH_HEADER_LEN + n.div_ceil(8), 0);
335 for (i, &ok) in accepted.iter().enumerate() {
336 if ok {
337 let (byte, mask) = bitmap_bit(i);
338 out[ACK_BATCH_HEADER_LEN + byte] |= mask;
339 }
340 }
341 out
342}
343
344/// Decodes an `AckBatch` payload into per-record outcomes.
345///
346/// `expected` is the record count the caller sent in its `PushBatch`. It is
347/// checked against the count echoed in the payload, because a bitmap is the
348/// first weir response whose meaning depends on client-held state: `Ack` means
349/// "your last record was accepted" and `AckTracked` carries its own coordinate,
350/// but a bitmap is meaningless without knowing which batch it answers. Since
351/// `ceil(N/8)` is not injective — N of 1017 through 1024 all yield 131 bytes —
352/// the echoed count is the only thing that can catch a desync.
353pub fn decode_ack_batch(payload: &[u8], expected: usize) -> Result<Vec<bool>, BatchError> {
354 if payload.len() < ACK_BATCH_HEADER_LEN {
355 return Err(BatchError::Truncated {
356 len: payload.len(),
357 need: ACK_BATCH_HEADER_LEN,
358 });
359 }
360 let version = payload[0];
361 if version != ACK_BATCH_VERSION {
362 return Err(BatchError::UnsupportedVersion { version });
363 }
364 let declared = u16::from_le_bytes([payload[1], payload[2]]) as usize;
365 if declared == 0 {
366 return Err(BatchError::EmptyBatch);
367 }
368 if declared != expected {
369 return Err(BatchError::LengthMismatch {
370 declared,
371 actual: expected,
372 });
373 }
374
375 let want = ACK_BATCH_HEADER_LEN + declared.div_ceil(8);
376 if payload.len() != want {
377 return Err(BatchError::LengthMismatch {
378 declared: want,
379 actual: payload.len(),
380 });
381 }
382
383 // Padding bits must be zero. Ignoring them would make `popcount == N` — the
384 // obvious way to ask "did the whole batch succeed" — silently wrong.
385 let used_in_last = declared % 8;
386 if used_in_last != 0 {
387 let last = payload[payload.len() - 1];
388 let padding_mask = !((1u8 << used_in_last) - 1);
389 if last & padding_mask != 0 {
390 return Err(BatchError::PaddingNotZero { byte: last });
391 }
392 }
393
394 let bits = &payload[ACK_BATCH_HEADER_LEN..];
395 Ok((0..declared)
396 .map(|i| {
397 let (byte, mask) = bitmap_bit(i);
398 bits[byte] & mask != 0
399 })
400 .collect())
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 const CAP: usize = 1024;
408 const REC: usize = 16 * 1024;
409
410 #[test]
411 fn a_batch_body_round_trips() {
412 let records: Vec<&[u8]> = vec![b"alpha", b"beta", b"gamma"];
413 let body = encode_batch_body(&records);
414 assert_eq!(decode_batch_body(&body, CAP, REC).unwrap(), records);
415 }
416
417 #[test]
418 fn a_single_record_batch_is_legal() {
419 let body = encode_batch_body(&[b"only"]);
420 assert_eq!(
421 decode_batch_body(&body, CAP, REC).unwrap(),
422 vec![&b"only"[..]]
423 );
424 }
425
426 #[test]
427 fn an_unknown_body_version_is_rejected_not_prefix_parsed() {
428 let mut body = encode_batch_body(&[b"x"]);
429 body[0] = 0x02;
430 assert_eq!(
431 decode_batch_body(&body, CAP, REC),
432 Err(BatchError::UnsupportedVersion { version: 0x02 })
433 );
434 }
435
436 #[test]
437 fn a_zero_record_batch_is_rejected() {
438 let body = encode_batch_body(&[]);
439 assert_eq!(
440 decode_batch_body(&body, CAP, REC),
441 Err(BatchError::EmptyBatch)
442 );
443 }
444
445 #[test]
446 fn a_count_over_the_cap_is_rejected_before_any_reservation() {
447 // Three bytes of input must not be able to reserve for 65,535 records.
448 let mut body = encode_batch_body(&[b"x"]);
449 body[1..3].copy_from_slice(&u16::MAX.to_le_bytes());
450 assert_eq!(
451 decode_batch_body(&body, CAP, REC),
452 Err(BatchError::TooManyRecords {
453 count: 65_535,
454 cap: CAP
455 })
456 );
457 }
458
459 #[test]
460 fn the_hard_cap_binds_even_when_the_caller_asks_for_more() {
461 let mut body = encode_batch_body(&[b"x"]);
462 body[1..3].copy_from_slice(&u16::MAX.to_le_bytes());
463 match decode_batch_body(&body, usize::MAX, REC) {
464 Err(BatchError::TooManyRecords { cap, .. }) => {
465 assert_eq!(cap, MAX_BATCH_RECORDS_HARD_CAP);
466 }
467 other => panic!("expected the hard cap to bind, got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn an_empty_record_inside_a_batch_is_rejected() {
473 // The WAB cannot represent one: a zero payload_len is its
474 // end-of-records sentinel. Catching it here is what keeps it from
475 // reaching flush_batch, where it would nack its neighbours.
476 let body = encode_batch_body(&[b"ok", b"", b"also ok"]);
477 assert_eq!(
478 decode_batch_body(&body, CAP, REC),
479 Err(BatchError::EmptyRecord { index: 1 })
480 );
481 }
482
483 #[test]
484 fn a_record_over_the_per_record_cap_is_rejected() {
485 // The frame cap bounds the BATCH; an operator setting max_payload_bytes
486 // is bounding a RECORD. Without this the setting silently stops meaning
487 // what three documents say it means.
488 let big = vec![b'x'; 64];
489 let body = encode_batch_body(&[b"small", &big]);
490 assert_eq!(
491 decode_batch_body(&body, CAP, 32),
492 Err(BatchError::RecordTooLarge {
493 index: 1,
494 len: 64,
495 cap: 32
496 })
497 );
498 }
499
500 #[test]
501 fn a_truncated_record_is_rejected() {
502 let mut body = encode_batch_body(&[b"alpha", b"beta"]);
503 body.truncate(body.len() - 2);
504 assert!(matches!(
505 decode_batch_body(&body, CAP, REC),
506 Err(BatchError::TruncatedRecord { .. })
507 ));
508 }
509
510 #[test]
511 fn trailing_bytes_after_the_last_record_are_rejected() {
512 let mut body = encode_batch_body(&[b"alpha"]);
513 body.push(0xAA);
514 assert!(matches!(
515 decode_batch_body(&body, CAP, REC),
516 Err(BatchError::TruncatedRecord { .. } | BatchError::LengthMismatch { .. })
517 ));
518 }
519
520 #[test]
521 fn a_count_that_disagrees_with_the_body_is_rejected_either_way() {
522 // Declared fewer than present.
523 let mut body = encode_batch_body(&[b"a", b"b", b"c"]);
524 body[1..3].copy_from_slice(&2u16.to_le_bytes());
525 assert!(matches!(
526 decode_batch_body(&body, CAP, REC),
527 Err(BatchError::LengthMismatch { .. })
528 ));
529
530 // Declared more than present.
531 let mut body = encode_batch_body(&[b"a", b"b"]);
532 body[1..3].copy_from_slice(&3u16.to_le_bytes());
533 assert_eq!(
534 decode_batch_body(&body, CAP, REC),
535 Err(BatchError::LengthMismatch {
536 declared: 3,
537 actual: 2
538 })
539 );
540 }
541
542 // ---- AckBatch bitmap ----
543
544 #[test]
545 fn an_ack_bitmap_round_trips() {
546 for n in [1usize, 7, 8, 9, 63, 64, 65, 255] {
547 let accepted: Vec<bool> = (0..n).map(|i| i % 3 != 0).collect();
548 let payload = encode_ack_batch(&accepted);
549 assert_eq!(
550 decode_ack_batch(&payload, n).unwrap(),
551 accepted,
552 "round trip failed at N={n}"
553 );
554 }
555 }
556
557 /// The test that catches a reversed bitmap.
558 ///
559 /// N=9 with only records 0 and 8 set is asymmetric across the byte
560 /// boundary, so LSB-first and MSB-first produce *different bytes*. Any
561 /// vector with N<=8, or a symmetric pattern, encodes identically under both
562 /// conventions and would pass while the implementation was inverted — and an
563 /// inverted bitmap is a well-formed frame that reports failures as
564 /// successes. That is a false ack, which no CRC or cap can detect.
565 #[test]
566 fn the_bitmap_is_lsb_first_and_the_bytes_say_so() {
567 let mut accepted = vec![false; 9];
568 accepted[0] = true;
569 accepted[8] = true;
570 let payload = encode_ack_batch(&accepted);
571
572 assert_eq!(payload[0], ACK_BATCH_VERSION);
573 assert_eq!(u16::from_le_bytes([payload[1], payload[2]]), 9);
574 assert_eq!(payload.len(), ACK_BATCH_HEADER_LEN + 2);
575
576 // LSB-first: record 0 is bit 0 of byte 0 -> 0b0000_0001.
577 // MSB-first would put it at 0b1000_0000 = 0x80.
578 assert_eq!(
579 payload[ACK_BATCH_HEADER_LEN], 0b0000_0001,
580 "record 0 must be the LEAST significant bit of the first byte"
581 );
582 assert_eq!(
583 payload[ACK_BATCH_HEADER_LEN + 1],
584 0b0000_0001,
585 "record 8 must be the least significant bit of the SECOND byte"
586 );
587 assert_eq!(decode_ack_batch(&payload, 9).unwrap(), accepted);
588 }
589
590 #[test]
591 fn a_set_padding_bit_is_rejected_rather_than_ignored() {
592 // Following ReservedFlagsSet: weir does not tolerate a set bit it has no
593 // meaning for. Ignoring padding would also make `popcount == N` — the
594 // obvious "did the whole batch succeed" check — silently wrong.
595 let accepted = vec![true; 9];
596 let mut payload = encode_ack_batch(&accepted);
597 let last = payload.len() - 1;
598 payload[last] |= 0b1000_0000;
599 assert!(matches!(
600 decode_ack_batch(&payload, 9),
601 Err(BatchError::PaddingNotZero { .. })
602 ));
603 }
604
605 #[test]
606 fn an_ack_for_a_different_batch_is_rejected() {
607 // ceil(N/8) is not injective: 1017..=1024 all give 128 bitmap bytes. The
608 // echoed count is the only thing that can catch a pipelined desync.
609 let accepted = vec![true; 1024];
610 let payload = encode_ack_batch(&accepted);
611 assert!(decode_ack_batch(&payload, 1024).is_ok());
612 assert_eq!(
613 decode_ack_batch(&payload, 1017),
614 Err(BatchError::LengthMismatch {
615 declared: 1024,
616 actual: 1017
617 })
618 );
619 }
620
621 #[test]
622 fn the_largest_ack_batch_fits_the_existing_response_bound() {
623 // The reason MAX_BATCH_RECORDS_HARD_CAP is 2048: weir's largest response
624 // payload must stay 298, so no client gains a new maximum to allocate
625 // for and no published size claim changes.
626 let accepted = vec![true; MAX_BATCH_RECORDS_HARD_CAP];
627 let payload = encode_ack_batch(&accepted);
628 assert_eq!(payload.len(), MAX_ACK_BATCH_PAYLOAD_LEN);
629 assert!(
630 payload.len() <= crate::MAX_TRACKED_ACK_PAYLOAD_LEN,
631 "the largest AckBatch ({}) exceeds the existing response cap ({}), \
632 which would force every client to grow its bound",
633 payload.len(),
634 crate::MAX_TRACKED_ACK_PAYLOAD_LEN
635 );
636 }
637}