You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@thrift.apache.org by "Jens Geyer (Jira)" <ji...@apache.org> on 2026/09/11 15:08:00 UTC
[jira] [Created] (THRIFT-6243) Grow the TNonblockingServer read buffer as the payload arrives, not on the frame header
Jens Geyer created THRIFT-6243:
----------------------------------
Summary: Grow the TNonblockingServer read buffer as the payload arrives, not on the frame header
Key: THRIFT-6243
URL: https://issues.apache.org/jira/browse/THRIFT-6243
Project: Thrift
Issue Type: Bug
Components: C++ - Library
Reporter: Jens Geyer
{{TNonblockingServer}}'s {{TConnection}} reserves the whole declared frame the moment it reads the four-byte length prefix: {{transition()}} in the {{APP_READ_FRAME_SIZE}} state reallocates the read buffer to the frame size ({{TNonblockingServer.cpp}}) before any payload byte has arrived. A peer can therefore make the server reserve up to {{getMaxFrameSize()}} bytes per connection by sending only a four-byte header, with {{MAX_CONNECTIONS}} defaulting to {{INT_MAX}}. The reservation is address space rather than resident memory (it is a {{std::realloc}}), but it is still an unamplified header-to-allocation step.
This is part (4) of the nonblocking-server frame handling. THRIFT-6183 lowered the default limit; a separate change wires the server to a {{TConfiguration}} and adds a pre-allocation budget check. This change bounds the allocation by what the peer actually sends:
* {{transition()}} reserves only enough to hold the length prefix and begin reading (the prefix plus about a kilobyte), not the declared frame.
* The buffer grows toward the frame size in the {{SOCKET_RECV}} state as bytes are actually read -- by the same doubling the up-front allocation used, now driven by what the peer sends rather than by the size it declares, with the doubling guarded so it cannot overflow {{uint32_t}}.
A frame that fully arrives ends at the same buffer size as before (the next power of two at or above the frame size), so there is no change for legitimate traffic in the steady state and no change at all for frames of a kilobyte or less (still a single allocation). A header with no payload reserves next to nothing.
Tradeoff, for the reviewer: a large frame that does arrive now pays the amortized doubling copy that any grow-as-you-go buffer pays -- on the order of twice the frame size in {{memmove}} over the life of the connection, against a single allocation before. This is the cost of not trusting the declared size, and the same shape the scan that raised this recommended ("allocate as the payload arrives, not on the header").
Test: TNonblockingServerTest gains three cases. A Linux-only, forked case announces a 256 MiB frame, sends none of it, and asserts the server's VmSize does not grow by more than a small slack -- it reads VmSize rather than RSS because the reservation is committed, not resident; it fails when the buffer is reserved on the header and passes when it grows with the payload. Two portable cases drive multi-megabyte requests through the framed client -- each frame far larger than the initial reservation, so it is assembled over many libevent callbacks with the buffer doubling as the bytes arrive, and every request must round-trip byte-for-byte. One frees the grown buffer when the connection closes (via {{returnConnection}}) and regrows it on the next connection; the other forces the resize counter to 1 so the buffer is freed between two frames on a single live connection and regrown from empty mid-stream. Together they cover the grow, the free, and the regrow-from-empty paths, so a lost byte or a mishandled empty buffer shows up as a mismatch.
_Drafted with AI assistance (Claude Opus 4.8)._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)