Repository navigation
perf(thrift): buffer reply body, use accelerated protocol, log payloads lazily - #1020
Conversation
…ds lazily THttpClient.read() forwarded every primitive read of the pure-Python thrift decoder to urllib3, which dominated decode time for wide result schemas and column-based metadata results. Read the reply body once, expose it as a CReadableTransport, and decode with TBinaryProtocolAccelerated (falls back to pure Python when thrift's C extension is unavailable). unsafe_logger.debug() pre-formatted every request and response with str.format, stringifying whole payloads (including inline Arrow batches) even though the logger only has a NullHandler by default. Pass them as %s args. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low
Looks good overall: the buffered CReadableTransport and the lazy %s logging are both correct, and the existing string/container limits are kept. One medium concern: the buffered reply body stays referenced after decoding, which roughly doubles memory held per connection for large inline results. There is also a minor inconsistency in the exception raised for a truncated reply.
Locks in that the transport's reply buffer is cleared once a successful RPC returns (make_request closes the transport in its finally block). Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
There was a problem hiding this comment.
Verdict: 2 Low
Looks good. Buffering the body and using the accelerated protocol are both correct, and the transport is closed in make_request's finally, which releases the buffer. There are two low-severity notes: peak memory now roughly doubles for large inline replies, and the "C path" test can pass without the C extension ever running.
TBinaryProtocolAccelerated silently falls back to pure Python when thrift's C extension isn't built, so add a test (skipped without fastbinary) that asserts the C decoder actually ran on the buffered body. Share reply/request builders across the transport tests, and note in the transport that the raw reply is held alongside the decoded objects until close(). Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
aakash-saravanan-db
left a comment
There was a problem hiding this comment.
Reviewed response buffering, accelerated protocol compatibility, and lazy logging. One native-parameter compatibility issue is noted inline. The backend suite passed with 179 tests and 95 subtests on each of Thrift 0.24 and 0.25.
TBinaryProtocolAccelerated also moved request encoding to fastbinary, whose recursion limit rejects a native array parameter nested 62+ levels once it is wrapped in ExecuteStatement_args. Main's pure-Python encoder accepts it, and a warehouse runs such queries at depths 62 and 63. Clear the protocol's _fast_encode so requests serialize as before, while replies still decode in C. Requests are small, so this has no measurable cost. Replace the C-vs-Python request byte comparison with a test that sends a 63-level nested parameter through make_request and checks the wire bytes. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
There was a problem hiding this comment.
Verdict: 2 Low
The change looks correct. I found two low-severity issues: the C encoder is disabled through a private thrift attribute, and the test guarding that only works when fastbinary is built. The chunked read/cstringio_refill logic correctly handles short reads, EOF, fields larger than a chunk, and switching between the pure-Python and C decode paths, and the lazy %s logging keeps the same output.
Instead of clearing thrift's private _fast_encode attribute, pass the generated client a TBinaryProtocolAccelerated for replies and a plain TBinaryProtocol for requests. The nested-parameter test now also checks the protocol types, so it guards the split where fastbinary isn't built. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
What type of PR is this?
Description
Two client-side costs on the Thrift backend showed up in the nightly driver benchmark:
THttpClient.read()forwarded every primitive read of the pure-Python Thrift decoder to urllib3. For replies with many fields (wide result schemas, column-based metadata results) that per-call overhead dominated: decoding a 2,415-rowgetColumnsreply took ~370 ms vs ~1.3 ms from a buffered body with the C decoder. The transport now reads the body from urllib3 in 1 MiB chunks (or a whole field if larger), likeTBufferedTransport, and implementsCReadableTransport. Because the body is never read whole, a 503/429 with a truncated body still reaches the status-based retry on urllib3 1.26, and the extra memory during decode is about one chunk or one field rather than the whole reply. The backend decodes replies withTBinaryProtocolAccelerated, which falls back to pure Python when Thrift's C extension isn't built. Requests are still encoded in pure Python, because the C encoder rejects structs nested deeper than about 64 levels, such as native array parameters nested 62 or more levels. String/container limits are unchanged; >15.6 MiB fields still decode on thrift 0.24 and 0.25.unsafe_logger.debug("...".format(payload))stringified every request and response even though that logger only has aNullHandlerby default (~25 ms per MB of inline Arrow). The payloads are now passed as%sargs, so output is unchanged when a handler is attached.Live A/B against a Medium PRO warehouse (p50 of 3 interleaved rounds, main → this PR):
tables()across all catalogs (60k rows)columns()onsamples(2.5k rows)LIMIT 0query, client time minus server timeSELECT 1How is this tested?
New unit tests cover the chunked transport under both the C and the pure-Python protocol, a full RPC decoded through the C path, the truncated-503 retry, and the lazy log calls. The unit suite passes on thrift 0.24 and 0.25, and with the C extension disabled. The Thrift e2e tests in
test_driver.py,test_complex_types.py,test_variant_types.pyandtest_parameterized_queries.pypass against a live warehouse. I didn't run the retry or long-running suites, or the UC volume and streaming-PUT tests, which need a volume fixture.Related Tickets & Documents
N/A
This pull request and its description were written by Isaac.