Skip to main content

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}