Skip to content

Commit 9ae3548

Browse files
vuanhphungIsaac
andcommitted
Cap the CloudFetch FetchResults budget at 4x buffer_size_bytes
Without a cap the budget grows with max_download_threads and the largest link seen, and it ignored a deliberately small buffer_size_bytes. The default path (about 210 MB, 10 links) is unchanged. Also document that buffer_size_bytes is no longer the max fetch size for CloudFetch. Signed-off-by: Vu Anh Phung <vu.phung@databricks.com> Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 650fd5d commit 9ae3548

3 files changed

Lines changed: 45 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Unreleased
44
- Fix inline parameters (`use_inline_params=True`) so a sequence directly after `IN` renders as a list again: `x IN %(ids)s` renders as `x IN (1,2,3)` instead of `x IN ARRAY(1,2,3)`, which Spark rejects. Inside the list, tuples render as rows for a multi-column `IN`, and a `set` or `frozenset` is accepted again. Sequences anywhere else still render as `ARRAY(...)` ([#629](https://github.com/databricks/databricks-sql-python/issues/629))
55
- Speed up Thrift response handling: read replies in 1 MiB chunks and decode them with Thrift's C-accelerated protocol, and stop stringifying every request/response for the default no-op debug logger. Wide result schemas, metadata calls with many rows, and inline Arrow results benefit most.
6-
- Speed up large CloudFetch results on Thrift: each FetchResults call now asks for about one result file per download thread instead of 100 MiB of files, so all download threads stay busy and the result takes fewer round trips.
6+
- Speed up large CloudFetch results on Thrift: each FetchResults call now asks for about one result file per download thread (at most 4x `buffer_size_bytes`) instead of 100 MiB of files, so all download threads stay busy and the result takes fewer round trips.
77

88
# 4.9.0 (2026-11-04)
99
- Upgrade Databricks SQL Kernel to 1.4.0, which keeps Lakehouse Real-Time session affinity and flushes shared telemetry only after the final session closes.

‎src/databricks/sql/result_set.py‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
logger = logging.getLogger(__name__)
3030

31+
# Upper bound on a Thrift CloudFetch FetchResults request, in multiples of buffer_size_bytes.
32+
_CLOUDFETCH_MAX_BUFFERS_PER_FETCH = 4
33+
3134

3235
class ResultSet(ABC):
3336
"""
@@ -60,7 +63,9 @@ def __init__(
6063
:param connection: The parent connection that was used to execute this command
6164
:param backend: The specialised backend client to be invoked in the fetch phase
6265
:param arraysize: The max number of rows to fetch at a time (PEP-249)
63-
:param buffer_size_bytes: The size (in bytes) of the internal buffer + max fetch
66+
:param buffer_size_bytes: The size (in bytes) of the internal buffer + max fetch.
67+
Thrift CloudFetch fetches may request up to 4x this (see
68+
ThriftResultSet._fetch_max_bytes).
6469
:param command_id: The command ID
6570
:param status: The command status
6671
:param has_been_closed_server_side: Whether the command has been closed on the server
@@ -217,7 +222,8 @@ def __init__(
217222
:param connection: The parent connection
218223
:param execute_response: Response from the execute command
219224
:param thrift_client: The ThriftDatabricksClient instance for direct access
220-
:param buffer_size_bytes: Buffer size for fetching results
225+
:param buffer_size_bytes: Buffer size for fetching results. CloudFetch
226+
fetches request between 1x and 4x this (see _fetch_max_bytes).
221227
:param arraysize: Default number of rows to fetch
222228
:param use_cloud_fetch: Whether to use cloud fetch for retrieving results
223229
:param t_row_set: The TRowSet containing result data (if available)
@@ -288,8 +294,12 @@ def _fetch_max_bytes(self) -> int:
288294
# file data, so buffer_size_bytes alone yields only a few links per round trip.
289295
# Ask for about one link per download thread: every link in the batch then starts
290296
# downloading immediately, so none sits waiting while it ages towards expiry.
291-
return max(
292-
self.buffer_size_bytes, self._max_download_threads * self._max_link_bytes
297+
# The cap bounds the batch (and buffered downloads) for a large
298+
# max_download_threads, an oversized link, or a deliberately small buffer.
299+
wanted = self._max_download_threads * self._max_link_bytes
300+
return min(
301+
max(self.buffer_size_bytes, wanted),
302+
_CLOUDFETCH_MAX_BUFFERS_PER_FETCH * self.buffer_size_bytes,
293303
)
294304

295305
def _fill_results_buffer(self):

‎tests/unit/test_fetches.py‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,11 @@ def _cloudfetch_queue(cls, *link_sizes):
415415
return queue
416416

417417
def _make_result_set(
418-
self, initial_link_sizes=None, fetched_queues=(), max_download_threads=10
418+
self,
419+
initial_link_sizes=None,
420+
fetched_queues=(),
421+
max_download_threads=10,
422+
buffer_size_bytes=BUFFER,
419423
):
420424
"""Returns the result set and the kwargs of every fetch_results call."""
421425
calls = []
@@ -449,7 +453,7 @@ def fetch_results(**kwargs):
449453
result_format=ttypes.TSparkRowSetType.URL_BASED_SET,
450454
),
451455
thrift_client=backend,
452-
buffer_size_bytes=self.BUFFER,
456+
buffer_size_bytes=buffer_size_bytes,
453457
arraysize=1000,
454458
t_row_set=t_row_set,
455459
max_download_threads=max_download_threads,
@@ -477,12 +481,34 @@ def test_scales_with_max_download_threads(self):
477481
rs, calls = self._make_result_set(
478482
initial_link_sizes=[20 * self.MIB],
479483
fetched_queues=[self._cloudfetch_queue()],
480-
max_download_threads=25,
484+
max_download_threads=15,
481485
)
482486

483487
rs._fill_results_buffer()
484488

485-
self.assertEqual(calls[0]["max_bytes"], 500 * self.MIB)
489+
self.assertEqual(calls[0]["max_bytes"], 300 * self.MIB)
490+
491+
def test_caps_at_four_buffers(self):
492+
rs, calls = self._make_result_set(
493+
initial_link_sizes=[20 * self.MIB],
494+
fetched_queues=[self._cloudfetch_queue()],
495+
max_download_threads=50,
496+
)
497+
498+
rs._fill_results_buffer()
499+
500+
self.assertEqual(calls[0]["max_bytes"], 4 * self.BUFFER)
501+
502+
def test_small_buffer_size_caps_batch(self):
503+
rs, calls = self._make_result_set(
504+
initial_link_sizes=[20 * self.MIB],
505+
fetched_queues=[self._cloudfetch_queue()],
506+
buffer_size_bytes=10 * self.MIB,
507+
)
508+
509+
rs._fill_results_buffer()
510+
511+
self.assertEqual(calls[0]["max_bytes"], 40 * self.MIB)
486512

487513
def test_never_requests_less_than_buffer_size(self):
488514
rs, calls = self._make_result_set(

0 commit comments

Comments
 (0)