Skip to main content

dynamo_runtime/pipeline/network/codec/
zero_copy_decoder.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Zero-copy TCP message decoder for high-concurrency scenarios
5//!
6//! This decoder eliminates message reconstruction copies by:
7//! 1. Reading into a reusable buffer
8//! 2. Parsing headers in-place
9//! 3. Splitting off exact message sizes (zero-copy via Bytes::split_to)
10//! 4. Returning Arc-counted Bytes that can be cloned cheaply
11
12use super::{
13    check_tcp_request_max_message_size, parse_tcp_request_frame_header, tcp_request_endpoint_len,
14    tcp_request_header_size, tcp_request_headers_len,
15};
16use crate::config::environment_names::request_plane::DYN_TCP_SHRINK_MESSAGE_SIZE;
17use crate::pipeline::network::get_tcp_max_message_size;
18use bytes::{Bytes, BytesMut};
19use std::io;
20use std::sync::OnceLock;
21use tokio::io::{AsyncRead, AsyncReadExt};
22
23const INITIAL_BUFFER_SIZE: usize = 262144; // 256KB
24const DEFAULT_SHRINK_SIZE: usize = 8 * 1024 * 1024; // 8MB
25
26static SHRINK_MESSAGE_SIZE: OnceLock<usize> = OnceLock::new();
27
28/// Get the shrink message size threshold.
29fn get_shrink_message_size() -> usize {
30    *SHRINK_MESSAGE_SIZE.get_or_init(|| {
31        let max_size = get_tcp_max_message_size();
32        // Check for environment variable override
33        let env_result = std::env::var(DYN_TCP_SHRINK_MESSAGE_SIZE);
34        let env_shrink_size = env_result.as_ref().ok().and_then(|s| {
35            s.parse::<usize>().ok().or_else(|| {
36                tracing::warn!(
37                    env_var = DYN_TCP_SHRINK_MESSAGE_SIZE,
38                    value = %s,
39                    "Invalid value for DYN_TCP_SHRINK_MESSAGE_SIZE, using default"
40                );
41                None
42            })
43        });
44
45        let resolved = resolve_shrink_message_size(max_size, env_shrink_size);
46
47        // Warn if the configured value was clamped
48        if let Some(configured) = env_shrink_size
49            && configured != resolved
50        {
51            tracing::warn!(
52                configured_size = configured,
53                resolved_size = resolved,
54                max_size = max_size,
55                initial_buffer_size = INITIAL_BUFFER_SIZE,
56                "DYN_TCP_SHRINK_MESSAGE_SIZE was clamped to valid range. Note the size is in bytes."
57            );
58        }
59
60        resolved
61    })
62}
63
64/// Resolve the shrink message size threshold based on configuration and constraints.
65///
66fn resolve_shrink_message_size(max_size: usize, env_shrink_size: Option<usize>) -> usize {
67    let configured_size = env_shrink_size.unwrap_or(DEFAULT_SHRINK_SIZE);
68
69    // Clamp to valid range: [INITIAL_BUFFER_SIZE, max_size]
70    configured_size
71        .min(max_size) // Don't exceed max message size
72        .max(INITIAL_BUFFER_SIZE) // Don't go below initial buffer size
73}
74
75/// Zero-copy streaming decoder that reuses buffers
76///
77/// This decoder maintains an internal buffer and only allocates when necessary.
78/// Messages are returned as Arc-counted Bytes slices, making cloning extremely cheap.
79/// The reusable buffer resets back to INITIAL_BUFFER_SIZE only when unread data
80/// is empty and capacity exceeds DYN_TCP_SHRINK_MESSAGE_SIZE.
81pub struct ZeroCopyTcpDecoder {
82    /// Reusable read buffer - grows as needed, shrinks when empty and oversized
83    read_buffer: BytesMut,
84    /// Maximum allowed message size
85    max_message_size: usize,
86    /// Threshold for shrinking buffer back to initial size when empty
87    shrink_threshold: usize,
88}
89
90impl ZeroCopyTcpDecoder {
91    /// Create a new decoder with default buffer size
92    pub fn new() -> Self {
93        Self::with_capacity(INITIAL_BUFFER_SIZE)
94    }
95
96    /// Create a new decoder with specific initial capacity
97    pub fn with_capacity(capacity: usize) -> Self {
98        Self {
99            read_buffer: BytesMut::with_capacity(capacity),
100            max_message_size: get_tcp_max_message_size(),
101            shrink_threshold: get_shrink_message_size(),
102        }
103    }
104
105    /// Read one complete message with ZERO copies
106    ///
107    /// This method:
108    /// 1. Ensures headers are buffered
109    /// 2. Parses headers in-place (no allocation)
110    /// 3. Ensures entire message is buffered
111    /// 4. Splits off exact message size (zero-copy pointer arithmetic)
112    /// 5. Returns Arc-counted Bytes (cheap to clone)
113    pub async fn read_message<R: AsyncRead + Unpin>(
114        &mut self,
115        reader: &mut R,
116    ) -> io::Result<TcpRequestMessageZeroCopy> {
117        // Fill buffer if needed
118        while self.read_buffer.len() < super::TCP_REQUEST_ENDPOINT_LEN_WIDTH {
119            let n = reader.read_buf(&mut self.read_buffer).await?;
120            if n == 0 {
121                if self.read_buffer.is_empty() {
122                    return Err(io::Error::new(
123                        io::ErrorKind::UnexpectedEof,
124                        "connection closed",
125                    ));
126                } else {
127                    return Err(io::Error::new(
128                        io::ErrorKind::UnexpectedEof,
129                        "incomplete message header",
130                    ));
131                }
132            }
133        }
134
135        // Parse endpoint path length (first 2 bytes) - NO COPY
136        let path_len = tcp_request_endpoint_len(&self.read_buffer)?;
137
138        // Ensure we have path + headers_len
139        let initial_header_size =
140            super::TCP_REQUEST_ENDPOINT_LEN_WIDTH + path_len + super::TCP_REQUEST_HEADERS_LEN_WIDTH;
141        while self.read_buffer.len() < initial_header_size {
142            let n = reader.read_buf(&mut self.read_buffer).await?;
143            if n == 0 {
144                return Err(io::Error::new(
145                    io::ErrorKind::UnexpectedEof,
146                    "incomplete message header",
147                ));
148            }
149        }
150
151        // Parse headers length (2 bytes after path) - NO COPY
152        let headers_len = tcp_request_headers_len(&self.read_buffer, path_len)?;
153
154        // Ensure we have headers + payload length
155        let full_header_size = tcp_request_header_size(path_len, headers_len);
156        while self.read_buffer.len() < full_header_size {
157            let n = reader.read_buf(&mut self.read_buffer).await?;
158            if n == 0 {
159                return Err(io::Error::new(
160                    io::ErrorKind::UnexpectedEof,
161                    "incomplete message header",
162                ));
163            }
164        }
165
166        let parsed = parse_tcp_request_frame_header(&self.read_buffer)?;
167
168        // Sanity check total message length (including all overhead)
169        check_tcp_request_max_message_size(parsed.total_len, self.max_message_size)?;
170
171        // Ensure entire message is buffered
172        while self.read_buffer.len() < parsed.total_len {
173            let n = reader.read_buf(&mut self.read_buffer).await?;
174            if n == 0 {
175                return Err(io::Error::new(
176                    io::ErrorKind::UnexpectedEof,
177                    format!(
178                        "incomplete message: expected {} bytes, got {}",
179                        parsed.total_len,
180                        self.read_buffer.len()
181                    ),
182                ));
183            }
184        }
185
186        // Split off exactly what we need - ZERO COPY!
187        // split_to() just advances the internal pointer, doesn't allocate or copy
188        let message_bytes = self.read_buffer.split_to(parsed.total_len).freeze();
189
190        // Shrink buffer if it grew too large and is now empty, could be optimized with lock-free buffer pool in the future.
191        if self.read_buffer.is_empty() && self.read_buffer.capacity() > self.shrink_threshold {
192            self.read_buffer = BytesMut::with_capacity(INITIAL_BUFFER_SIZE);
193        }
194
195        // Return zero-copy message wrapper
196        Ok(TcpRequestMessageZeroCopy::new(message_bytes, parsed))
197    }
198
199    /// Get the current buffer capacity
200    pub fn buffer_capacity(&self) -> usize {
201        self.read_buffer.capacity()
202    }
203
204    /// Get the current buffered data size
205    pub fn buffered_len(&self) -> usize {
206        self.read_buffer.len()
207    }
208}
209
210impl Default for ZeroCopyTcpDecoder {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216/// Zero-copy message representation
217///
218/// This struct holds an Arc-counted Bytes buffer containing the entire message.
219/// All accessors return zero-copy slices or references into this buffer.
220#[derive(Clone)]
221pub struct TcpRequestMessageZeroCopy {
222    /// Entire message as Arc-counted buffer
223    /// Format: [path_len(2)][path(var)][headers_len(2)][headers(var)][payload_len(4)][payload(var)]
224    raw: Bytes,
225    parsed: super::TcpRequestWireHeader,
226}
227
228impl TcpRequestMessageZeroCopy {
229    /// Create a new zero-copy message from raw bytes
230    fn new(raw: Bytes, parsed: super::TcpRequestWireHeader) -> Self {
231        Self { raw, parsed }
232    }
233
234    /// Get endpoint path as a string slice (zero-copy)
235    ///
236    /// This returns a reference into the message buffer, no allocation.
237    pub fn endpoint_path(&self) -> Result<&str, std::str::Utf8Error> {
238        std::str::from_utf8(&self.raw[self.parsed.endpoint_start()..self.parsed.endpoint_end()])
239    }
240
241    /// Get endpoint path as bytes (zero-copy)
242    pub fn endpoint_path_bytes(&self) -> &[u8] {
243        &self.raw[self.parsed.endpoint_start()..self.parsed.endpoint_end()]
244    }
245
246    /// Get headers as bytes (zero-copy)
247    pub fn headers_bytes(&self) -> &[u8] {
248        &self.raw[self.parsed.headers_start()..self.parsed.headers_end()]
249    }
250
251    /// Get headers as a HashMap (requires parsing)
252    pub fn headers(&self) -> std::collections::HashMap<String, String> {
253        let headers_bytes = self.headers_bytes();
254        if headers_bytes.is_empty() {
255            return std::collections::HashMap::new();
256        }
257
258        // Parse headers from JSON format
259        serde_json::from_slice(headers_bytes).unwrap_or_default()
260    }
261
262    /// Get the payload length
263    #[inline]
264    fn payload_len(&self) -> usize {
265        self.parsed.payload_len
266    }
267
268    /// Get payload as zero-copy Bytes
269    ///
270    /// This returns an Arc-counted slice of the message buffer.
271    /// Cloning the returned Bytes is extremely cheap (just Arc clone).
272    pub fn payload(&self) -> Bytes {
273        self.raw.slice(self.parsed.payload_start()..) // ZERO COPY! Just Arc clone + offset
274    }
275
276    /// Get total message size in bytes
277    pub fn total_size(&self) -> usize {
278        self.raw.len()
279    }
280
281    /// Get the raw message bytes (for debugging)
282    pub fn raw_bytes(&self) -> &Bytes {
283        &self.raw
284    }
285}
286
287impl std::fmt::Debug for TcpRequestMessageZeroCopy {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        f.debug_struct("TcpRequestMessageZeroCopy")
290            .field("total_size", &self.total_size())
291            .field("endpoint_path", &self.endpoint_path().ok())
292            .field("payload_len", &self.payload_len())
293            .finish()
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use tokio::io::AsyncWriteExt;
301
302    #[test]
303    fn test_resolve_shrink_message_size_edge_cases() {
304        // Test case: max_size = 10MB (larger than DEFAULT_SHRINK_SIZE)
305        // Should return DEFAULT_SHRINK_SIZE (8MB) since env is None
306        let max_size_10mb = 10 * 1024 * 1024;
307        let result = resolve_shrink_message_size(max_size_10mb, None);
308        assert_eq!(
309            result, DEFAULT_SHRINK_SIZE,
310            "10MB max should return default 8MB"
311        );
312
313        // Test case: max_size < DEFAULT_SHRINK_SIZE
314        // Should return max_size (capped by .min())
315        let max_size_1mb = 1024 * 1024;
316        let result = resolve_shrink_message_size(max_size_1mb, None);
317        assert_eq!(result, max_size_1mb, "1MB max should be capped to 1MB");
318
319        // Test case: max_size = DEFAULT_SHRINK_SIZE
320        // Should return DEFAULT_SHRINK_SIZE (exact match)
321        let result = resolve_shrink_message_size(DEFAULT_SHRINK_SIZE, None);
322        assert_eq!(
323            result, DEFAULT_SHRINK_SIZE,
324            "exact match should return default"
325        );
326
327        // Test case: env_shrink_size provided and within bounds
328        let env_size = 2 * 1024 * 1024; // 2MB
329        let result = resolve_shrink_message_size(max_size_10mb, Some(env_size));
330        assert_eq!(
331            result, env_size,
332            "env var should be used when within bounds"
333        );
334
335        // Test case: env_shrink_size exceeds max_size
336        let env_size_large = 20 * 1024 * 1024; // 20MB
337        let result = resolve_shrink_message_size(max_size_10mb, Some(env_size_large));
338        assert_eq!(
339            result, max_size_10mb,
340            "env var should be capped to max_size"
341        );
342
343        // Test case: env_shrink_size below INITIAL_BUFFER_SIZE
344        let env_size_small = 100 * 1024; // 100KB < 256KB
345        let result = resolve_shrink_message_size(max_size_10mb, Some(env_size_small));
346        assert_eq!(
347            result, INITIAL_BUFFER_SIZE,
348            "env var should be clamped to INITIAL_BUFFER_SIZE"
349        );
350
351        // Test case: max_size below INITIAL_BUFFER_SIZE
352        let max_size_small = 100 * 1024; // 100KB < 256KB
353        let result = resolve_shrink_message_size(max_size_small, None);
354        assert_eq!(
355            result, INITIAL_BUFFER_SIZE,
356            "result should be clamped to INITIAL_BUFFER_SIZE"
357        );
358    }
359
360    #[tokio::test]
361    async fn test_zero_copy_decoder_basic() {
362        // Create a test message with headers
363        let endpoint = "test/endpoint";
364        let payload = b"Hello, World!";
365        let headers: Vec<u8> = vec![]; // Empty headers
366
367        let mut message = Vec::new();
368        // path_len + path
369        message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
370        message.extend_from_slice(endpoint.as_bytes());
371        // headers_len + headers
372        message.extend_from_slice(&(headers.len() as u16).to_be_bytes());
373        message.extend_from_slice(&headers);
374        // payload_len + payload
375        message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
376        message.extend_from_slice(payload);
377
378        // Create a mock reader
379        let mut reader = &message[..];
380
381        // Decode
382        let mut decoder = ZeroCopyTcpDecoder::new();
383        let msg = decoder.read_message(&mut reader).await.unwrap();
384
385        // Verify
386        assert_eq!(msg.endpoint_path().unwrap(), endpoint);
387        assert_eq!(msg.payload().as_ref(), payload);
388        assert_eq!(msg.total_size(), message.len());
389        assert_eq!(msg.headers().len(), 0); // Empty headers
390    }
391
392    #[tokio::test]
393    async fn test_zero_copy_decoder_allows_empty_and_long_endpoint_paths() {
394        for endpoint in [String::new(), "x".repeat(2048)] {
395            let payload = b"payload";
396
397            let mut message = Vec::new();
398            message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
399            message.extend_from_slice(endpoint.as_bytes());
400            message.extend_from_slice(&(0u16).to_be_bytes());
401            message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
402            message.extend_from_slice(payload);
403
404            let mut reader = &message[..];
405            let mut decoder = ZeroCopyTcpDecoder::new();
406            let msg = decoder.read_message(&mut reader).await.unwrap();
407
408            assert_eq!(msg.endpoint_path().unwrap(), endpoint.as_str());
409            assert_eq!(msg.payload().as_ref(), payload);
410        }
411    }
412
413    #[tokio::test]
414    async fn test_zero_copy_decoder_large_payload() {
415        // Create a large payload (200KB)
416        let endpoint = "large/endpoint";
417        let payload = vec![0x42u8; 200 * 1024];
418        let headers: Vec<u8> = vec![]; // Empty headers
419
420        let mut message = Vec::new();
421        // path_len + path
422        message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
423        message.extend_from_slice(endpoint.as_bytes());
424        // headers_len + headers
425        message.extend_from_slice(&(headers.len() as u16).to_be_bytes());
426        message.extend_from_slice(&headers);
427        // payload_len + payload
428        message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
429        message.extend_from_slice(&payload);
430
431        let mut reader = &message[..];
432        let mut decoder = ZeroCopyTcpDecoder::new();
433        let msg = decoder.read_message(&mut reader).await.unwrap();
434
435        assert_eq!(msg.endpoint_path().unwrap(), endpoint);
436        assert_eq!(msg.payload().len(), payload.len());
437    }
438
439    #[tokio::test]
440    async fn test_zero_copy_decoder_total_size_limit() {
441        // Test that the decoder validates total message size, not just payload size
442        // Create a message where total_len exceeds max but payload alone might not
443        let max_size = 1024; // 1KB limit
444        let mut decoder = ZeroCopyTcpDecoder::with_capacity(256);
445        decoder.max_message_size = max_size;
446
447        // Create a message that exceeds the limit with overhead included
448        let endpoint = "test/endpoint";
449        let payload = vec![0x42u8; max_size]; // Payload equals max
450        let headers: Vec<u8> = vec![]; // Empty headers
451
452        let mut message = Vec::new();
453        // path_len + path
454        message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
455        message.extend_from_slice(endpoint.as_bytes());
456        // headers_len + headers
457        message.extend_from_slice(&(headers.len() as u16).to_be_bytes());
458        message.extend_from_slice(&headers);
459        // payload_len + payload
460        message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
461        message.extend_from_slice(&payload);
462
463        // total_len = 2 + 13 + 2 + 0 + 4 + 1024 = 1045 bytes > 1024 max
464        let mut reader = &message[..];
465        let result = decoder.read_message(&mut reader).await;
466
467        // Should fail with InvalidData error
468        assert!(result.is_err());
469        let err = result.unwrap_err();
470        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
471        assert!(err.to_string().contains("message too large"));
472        assert!(err.to_string().contains("1045")); // total_len
473        assert!(err.to_string().contains("1024")); // max_message_size
474    }
475
476    #[tokio::test]
477    async fn test_zero_copy_decoder_with_headers() {
478        // Test header parsing with actual header data
479        let endpoint = "api/v1/inference";
480        let payload = b"Request payload data";
481
482        // Create mock headers as JSON
483        let mut headers_map = std::collections::HashMap::new();
484        headers_map.insert("traceparent".to_string(), "00-abc123-def456-01".to_string());
485        headers_map.insert("user-agent".to_string(), "test-client/1.0".to_string());
486        headers_map.insert("request-id".to_string(), "req-12345".to_string());
487
488        let headers_json = serde_json::to_vec(&headers_map).unwrap();
489
490        let mut message = Vec::new();
491        // path_len + path
492        message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
493        message.extend_from_slice(endpoint.as_bytes());
494        // headers_len + headers (non-empty this time)
495        message.extend_from_slice(&(headers_json.len() as u16).to_be_bytes());
496        message.extend_from_slice(&headers_json);
497        // payload_len + payload
498        message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
499        message.extend_from_slice(payload);
500
501        // Decode the message
502        let mut reader = &message[..];
503        let mut decoder = ZeroCopyTcpDecoder::new();
504        let msg = decoder.read_message(&mut reader).await.unwrap();
505
506        // Verify endpoint
507        assert_eq!(msg.endpoint_path().unwrap(), endpoint);
508
509        // Verify payload
510        assert_eq!(msg.payload().as_ref(), payload);
511
512        // Verify total size includes all components
513        assert_eq!(msg.total_size(), message.len());
514
515        // Verify headers are correctly parsed
516        let decoded_headers = msg.headers();
517        assert_eq!(decoded_headers.len(), 3);
518        assert_eq!(
519            decoded_headers.get("traceparent").unwrap(),
520            "00-abc123-def456-01"
521        );
522        assert_eq!(
523            decoded_headers.get("user-agent").unwrap(),
524            "test-client/1.0"
525        );
526        assert_eq!(decoded_headers.get("request-id").unwrap(), "req-12345");
527
528        // Verify headers_bytes returns the raw JSON
529        let headers_bytes = msg.headers_bytes();
530        assert_eq!(headers_bytes, &headers_json[..]);
531    }
532
533    #[tokio::test]
534    async fn test_zero_copy_decoder_empty_vs_populated_headers() {
535        // Test both empty and populated headers in sequence to ensure proper parsing
536        let endpoint = "test/endpoint";
537        let payload = b"test data";
538
539        // Test 1: Empty headers
540        let mut message_empty = Vec::new();
541        message_empty.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
542        message_empty.extend_from_slice(endpoint.as_bytes());
543        message_empty.extend_from_slice(&(0u16).to_be_bytes()); // headers_len = 0
544        // No headers bytes
545        message_empty.extend_from_slice(&(payload.len() as u32).to_be_bytes());
546        message_empty.extend_from_slice(payload);
547
548        let mut reader = &message_empty[..];
549        let mut decoder = ZeroCopyTcpDecoder::new();
550        let msg = decoder.read_message(&mut reader).await.unwrap();
551
552        assert_eq!(msg.endpoint_path().unwrap(), endpoint);
553        assert_eq!(msg.payload().as_ref(), payload);
554        assert_eq!(msg.headers().len(), 0);
555        assert_eq!(msg.headers_bytes().len(), 0);
556
557        // Test 2: Populated headers with same decoder
558        let mut headers_map = std::collections::HashMap::new();
559        headers_map.insert("x-test-header".to_string(), "test-value".to_string());
560        let headers_json = serde_json::to_vec(&headers_map).unwrap();
561
562        let mut message_with_headers = Vec::new();
563        message_with_headers.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
564        message_with_headers.extend_from_slice(endpoint.as_bytes());
565        message_with_headers.extend_from_slice(&(headers_json.len() as u16).to_be_bytes());
566        message_with_headers.extend_from_slice(&headers_json);
567        message_with_headers.extend_from_slice(&(payload.len() as u32).to_be_bytes());
568        message_with_headers.extend_from_slice(payload);
569
570        let mut reader = &message_with_headers[..];
571        let msg = decoder.read_message(&mut reader).await.unwrap();
572
573        assert_eq!(msg.endpoint_path().unwrap(), endpoint);
574        assert_eq!(msg.payload().as_ref(), payload);
575        assert_eq!(msg.headers().len(), 1);
576        assert_eq!(msg.headers().get("x-test-header").unwrap(), "test-value");
577    }
578
579    #[tokio::test]
580    async fn test_zero_copy_decoder_buffer_shrinking() {
581        // Test that buffer shrinks back after reading a large message.
582        // Uses small sizes to avoid env var dependencies and keep test fast.
583        let endpoint = "test/endpoint";
584        let small_payload = b"small";
585        // Use 1MB payload with 512KB shrink threshold
586        let large_payload = vec![0x42u8; 1024 * 1024]; // 1MB
587
588        fn make_message(endpoint: &str, payload: &[u8]) -> Vec<u8> {
589            let mut message = Vec::new();
590            message.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
591            message.extend_from_slice(endpoint.as_bytes());
592            message.extend_from_slice(&(0u16).to_be_bytes()); // empty headers
593            message.extend_from_slice(&(payload.len() as u32).to_be_bytes());
594            message.extend_from_slice(payload);
595            message
596        }
597
598        // Create decoder with explicit settings to avoid env var dependencies
599        let mut decoder = ZeroCopyTcpDecoder::with_capacity(INITIAL_BUFFER_SIZE);
600        decoder.max_message_size = 2 * 1024 * 1024; // 2MB max
601        decoder.shrink_threshold = 512 * 1024; // 512KB shrink threshold
602
603        assert!(decoder.buffer_capacity() <= INITIAL_BUFFER_SIZE);
604
605        // Read large message - buffer grows during read, then shrinks after split_to()
606        let large_message = make_message(endpoint, &large_payload);
607        let mut reader = &large_message[..];
608        decoder.read_message(&mut reader).await.unwrap();
609
610        // After reading, buffer should have shrunk back because:
611        // - The buffer grew to ~1MB to hold the message
612        // - 1MB >= 512KB shrink threshold, so it triggers the shrink
613        assert!(
614            decoder.buffer_capacity() <= INITIAL_BUFFER_SIZE,
615            "buffer should shrink after large message, got capacity {}",
616            decoder.buffer_capacity()
617        );
618        assert!(
619            decoder.buffered_len() == 0,
620            "buffer should be empty after read"
621        );
622
623        // Read small message - should work fine with shrunk buffer
624        let small_message = make_message(endpoint, small_payload);
625        let mut reader = &small_message[..];
626        let msg = decoder.read_message(&mut reader).await.unwrap();
627        assert_eq!(msg.payload().as_ref(), small_payload);
628    }
629}