Skip to content

Commit 4ab0c54

Browse files
vuanhphungIsaac
andcommitted
test(thrift): assert the C decoder runs; document reply-buffer trade-off
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>
1 parent a1dd416 commit 4ab0c54

2 files changed

Lines changed: 65 additions & 49 deletions

File tree

‎src/databricks/sql/auth/thrift_http_client.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ def flush(self):
220220
self.headers = self.__resp.headers
221221

222222
# Thrift decodes the reply one field at a time; reading each field
223-
# straight from urllib3 costs far more than the decode itself.
223+
# straight from urllib3 costs far more than the decode itself. The
224+
# trade-off: the raw body (bounded by the request's maxBytes) is held
225+
# alongside the decoded objects until close() at the end of the RPC.
224226
self.__rbuf = BytesIO(self.__resp.read())
225227

226228
logger.info(

‎tests/unit/test_thrift_backend.py‎

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
from databricks.sql.result_set import ResultSet, ThriftResultSet
2323
from databricks.sql.backend.types import CommandId, CommandState, SessionId, BackendType
2424

25+
try:
26+
from thrift.protocol import fastbinary # noqa: F401
27+
28+
_HAS_THRIFT_FASTBINARY = True
29+
except ImportError:
30+
_HAS_THRIFT_FASTBINARY = False
31+
2532

2633
def retry_policy_factory():
2734
return { # (type, default, min, max)
@@ -386,19 +393,13 @@ def _http_client_with_reply(reply_bytes):
386393
http_client._THttpClient__pool.request.return_value = mock_resp
387394
return http_client, mock_resp
388395

389-
def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol(self):
390-
"""The reply body is read from urllib3 once, then decoded from memory,
391-
including by the C-accelerated protocol (the transport is a
392-
CReadableTransport) for payloads above thrift's ~15.6 MiB default limit."""
396+
@staticmethod
397+
def _fetch_results_reply(batch):
398+
"""Wire bytes of a FetchResults REPLY carrying one inline Arrow batch."""
393399
from thrift.protocol import TBinaryProtocol
394400
from thrift.transport import TTransport
395401
from databricks.sql.thrift_api.TCLIService import TCLIService
396-
from databricks.sql.backend.thrift_backend import (
397-
THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
398-
THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
399-
)
400402

401-
batch = b"x" * (20 * 1024 * 1024)
402403
resp = ttypes.TFetchResultsResp(
403404
status=ttypes.TStatus(statusCode=ttypes.TStatusCode.SUCCESS_STATUS),
404405
hasMoreRows=False,
@@ -413,31 +414,71 @@ def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol(self):
413414
reply_proto.writeMessageBegin("FetchResults", 2, 0) # REPLY
414415
TCLIService.FetchResults_result(success=resp).write(reply_proto)
415416
reply_proto.writeMessageEnd()
417+
return reply.getvalue()
418+
419+
@staticmethod
420+
def _fetch_results_req():
421+
op_handle = ttypes.TOperationHandle(
422+
operationId=ttypes.THandleIdentifier(guid=b"g" * 16, secret=b"s" * 16),
423+
operationType=ttypes.TOperationType.EXECUTE_STATEMENT,
424+
hasResultSet=True,
425+
)
426+
return ttypes.TFetchResultsReq(
427+
operationHandle=op_handle, orientation=0, maxRows=1
428+
)
429+
430+
def _accelerated_client(self, http_client):
431+
from thrift.protocol import TBinaryProtocol
432+
from databricks.sql.thrift_api.TCLIService import TCLIService
433+
from databricks.sql.backend.thrift_backend import (
434+
THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
435+
THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
436+
)
416437

417-
http_client, mock_resp = self._http_client_with_reply(reply.getvalue())
418-
self.assertIsInstance(http_client, TTransport.CReadableTransport)
419438
protocol = TBinaryProtocol.TBinaryProtocolAccelerated(
420439
http_client,
421440
string_length_limit=THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
422441
container_length_limit=THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
423442
)
424-
client = TCLIService.Client(protocol)
443+
return protocol, TCLIService.Client(protocol)
425444

426-
op_handle = ttypes.TOperationHandle(
427-
operationId=ttypes.THandleIdentifier(guid=b"g" * 16, secret=b"s" * 16),
428-
operationType=ttypes.TOperationType.EXECUTE_STATEMENT,
429-
hasResultSet=True,
430-
)
431-
result = client.FetchResults(
432-
ttypes.TFetchResultsReq(operationHandle=op_handle, orientation=0, maxRows=1)
445+
def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol(self):
446+
"""The reply body is read from urllib3 once and decoded from memory via
447+
TBinaryProtocolAccelerated, for payloads above thrift's ~15.6 MiB
448+
default limit. Runs on both the C and the pure-Python fallback path."""
449+
from thrift.transport import TTransport
450+
451+
batch = b"x" * (20 * 1024 * 1024)
452+
http_client, mock_resp = self._http_client_with_reply(
453+
self._fetch_results_reply(batch)
433454
)
455+
self.assertIsInstance(http_client, TTransport.CReadableTransport)
456+
_, client = self._accelerated_client(http_client)
457+
458+
result = client.FetchResults(self._fetch_results_req())
434459

435460
self.assertEqual(result.results.arrowBatches[0].batch, batch)
436461
mock_resp.read.assert_called_once_with()
437462

438463
http_client.close()
439464
self.assertEqual(http_client.read(1), b"")
440465

466+
@unittest.skipUnless(
467+
_HAS_THRIFT_FASTBINARY, "thrift C extension (fastbinary) not built"
468+
)
469+
def test_http_client_reply_is_decoded_by_thrift_c_extension(self):
470+
"""TBinaryProtocolAccelerated silently falls back to pure Python without
471+
fastbinary, so assert the C decoder actually ran on the buffered body."""
472+
batch = b"x" * (20 * 1024 * 1024)
473+
http_client, _ = self._http_client_with_reply(self._fetch_results_reply(batch))
474+
protocol, client = self._accelerated_client(http_client)
475+
protocol._fast_decode = Mock(wraps=protocol._fast_decode)
476+
477+
result = client.FetchResults(self._fetch_results_req())
478+
479+
self.assertEqual(result.results.arrowBatches[0].batch, batch)
480+
protocol._fast_decode.assert_called()
481+
441482
def test_http_client_cstringio_refill_on_truncated_reply_raises(self):
442483
http_client, _ = self._http_client_with_reply(b"")
443484
with self.assertRaises(EOFError):
@@ -446,26 +487,7 @@ def test_http_client_cstringio_refill_on_truncated_reply_raises(self):
446487
def test_reply_buffer_is_released_after_successful_make_request(self):
447488
"""make_request closes the transport on success too, so the buffered
448489
reply body is not kept on the connection after it has been decoded."""
449-
from thrift.protocol import TBinaryProtocol
450-
from thrift.transport import TTransport
451-
from databricks.sql.thrift_api.TCLIService import TCLIService
452-
453490
batch = b"x" * (1024 * 1024)
454-
resp = ttypes.TFetchResultsResp(
455-
status=ttypes.TStatus(statusCode=ttypes.TStatusCode.SUCCESS_STATUS),
456-
hasMoreRows=False,
457-
results=ttypes.TRowSet(
458-
startRowOffset=0,
459-
rows=[],
460-
arrowBatches=[ttypes.TSparkArrowBatch(batch=batch, rowCount=1)],
461-
),
462-
)
463-
reply = TTransport.TMemoryBuffer()
464-
reply_proto = TBinaryProtocol.TBinaryProtocol(reply)
465-
reply_proto.writeMessageBegin("FetchResults", 2, 0) # REPLY
466-
TCLIService.FetchResults_result(success=resp).write(reply_proto)
467-
reply_proto.writeMessageEnd()
468-
469491
thrift_backend = ThriftDatabricksClient(
470492
"foobar",
471493
443,
@@ -476,20 +498,12 @@ def test_reply_buffer_is_released_after_successful_make_request(self):
476498
http_client=MagicMock(),
477499
)
478500
mock_resp = Mock(status=200, reason="OK", headers={})
479-
mock_resp.read.return_value = reply.getvalue()
501+
mock_resp.read.return_value = self._fetch_results_reply(batch)
480502
thrift_backend._transport._THttpClient__pool = Mock()
481503
thrift_backend._transport._THttpClient__pool.request.return_value = mock_resp
482504

483-
op_handle = ttypes.TOperationHandle(
484-
operationId=ttypes.THandleIdentifier(guid=b"g" * 16, secret=b"s" * 16),
485-
operationType=ttypes.TOperationType.EXECUTE_STATEMENT,
486-
hasResultSet=True,
487-
)
488505
result = thrift_backend.make_request(
489-
thrift_backend._client.FetchResults,
490-
ttypes.TFetchResultsReq(
491-
operationHandle=op_handle, orientation=0, maxRows=1
492-
),
506+
thrift_backend._client.FetchResults, self._fetch_results_req()
493507
)
494508

495509
self.assertEqual(result.results.arrowBatches[0].batch, batch)

0 commit comments

Comments
 (0)