Skip to content

Commit 1fee752

Browse files
Skip unused Python HTTP client for explicit kernel connections (#1017)
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent 559f1d3 commit 1fee752

4 files changed

Lines changed: 174 additions & 5 deletions

File tree

‎src/databricks/sql/client.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,12 @@ def read(self) -> Optional[OAuthToken]:
413413
)
414414

415415
client_context = build_client_context(server_hostname, __version__, **kwargs)
416-
self.http_client = UnifiedHttpClient(client_context)
416+
# The kernel owns its HTTP transport.
417+
self.http_client: Optional[UnifiedHttpClient] = (
418+
None
419+
if kwargs.get("use_kernel", False)
420+
else UnifiedHttpClient(client_context)
421+
)
417422

418423
try:
419424
self.session = self._open_session_with_reyden_fallback(
@@ -483,10 +488,13 @@ def read(self) -> Optional[OAuthToken]:
483488
)
484489

485490
# Determine proxy usage
486-
use_proxy = self.http_client.using_proxy()
491+
use_proxy = (
492+
self.http_client.using_proxy() if self.http_client is not None else False
493+
)
487494
proxy_host_info = None
488495
if (
489-
use_proxy
496+
self.http_client is not None
497+
and use_proxy
490498
and self.http_client.proxy_uri
491499
and isinstance(self.http_client.proxy_uri, str)
492500
):
@@ -1386,6 +1394,8 @@ def _handle_staging_put(
13861394
session_id_hex=self.connection.get_session_id_hex(),
13871395
)
13881396

1397+
# Kernel backends reject staging operations before reaching these handlers.
1398+
assert self.connection.http_client is not None
13891399
with open(local_file, "rb") as fh:
13901400
r = self.connection.http_client.request(
13911401
HttpMethod.PUT, presigned_url, body=fh.read(), headers=headers
@@ -1444,6 +1454,7 @@ def _handle_staging_put_stream(
14441454
session_id_hex=self.connection.get_session_id_hex(),
14451455
)
14461456

1457+
assert self.connection.http_client is not None
14471458
r = self.connection.http_client.request(
14481459
HttpMethod.PUT, presigned_url, body=stream.read(), headers=headers
14491460
)
@@ -1466,6 +1477,7 @@ def _handle_staging_get(
14661477
session_id_hex=self.connection.get_session_id_hex(),
14671478
)
14681479

1480+
assert self.connection.http_client is not None
14691481
r = self.connection.http_client.request(
14701482
HttpMethod.GET, presigned_url, headers=headers
14711483
)
@@ -1490,6 +1502,7 @@ def _handle_staging_remove(
14901502
):
14911503
"""Make an HTTP DELETE request to the presigned_url"""
14921504

1505+
assert self.connection.http_client is not None
14931506
r = self.connection.http_client.request(
14941507
HttpMethod.DELETE, presigned_url, headers=headers
14951508
)

‎src/databricks/sql/session.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def __init__(
5252
self,
5353
server_hostname: str,
5454
http_path: str,
55-
http_client: UnifiedHttpClient,
55+
http_client: Optional[UnifiedHttpClient],
5656
http_headers: Optional[List[Tuple[str, str]]] = None,
5757
session_configuration: Optional[Dict[str, Any]] = None,
5858
catalog: Optional[str] = None,

‎tests/unit/test_kernel_async_readers.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from concurrent.futures import ThreadPoolExecutor
77
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
88
from types import SimpleNamespace
9+
from unittest.mock import patch
910
from urllib.parse import urlsplit
1011

1112
import pytest
@@ -132,7 +133,10 @@ def handle_request(self):
132133
def log_message(self, *_args):
133134
pass
134135

135-
with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server:
136+
with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server, patch(
137+
"databricks.sql.client.UnifiedHttpClient",
138+
side_effect=AssertionError("Explicit kernel must not create a Python client"),
139+
):
136140
worker = threading.Thread(target=server.serve_forever, daemon=True)
137141
worker.start()
138142
conn = None
@@ -144,6 +148,7 @@ def log_message(self, *_args):
144148
use_kernel=True,
145149
enable_telemetry=False,
146150
)
151+
assert conn.http_client is None
147152
yield conn, state
148153
finally:
149154
state.release.set()
@@ -180,6 +185,17 @@ def _assert_read_routes(state):
180185
assert execute_bodies[0]["wait_timeout"] == "0s"
181186

182187

188+
def test_sync_cursor_fetch_without_python_http_client(async_connection):
189+
conn, state = async_connection
190+
with conn.cursor() as cursor:
191+
cursor.execute("SELECT id AS n FROM range(1, 5)")
192+
assert cursor.fetchall() == [(1,), (2,), (3,), (4,)]
193+
assert any(
194+
method == "GET" and path.endswith("/result/chunks/1")
195+
for method, path, _ in state.requests
196+
)
197+
198+
183199
def test_async_cursor_refetch_preserves_session(async_connection):
184200
from databricks.sql.backend.types import CommandState
185201

‎tests/unit/test_session.py‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,60 @@ class TestSession:
3333
"enable_telemetry": False,
3434
}
3535

36+
@pytest.mark.parametrize(
37+
"backend_kwargs, backend_name",
38+
[
39+
({}, "ThriftDatabricksClient"),
40+
({"use_kernel": False}, "ThriftDatabricksClient"),
41+
({"use_sea": True}, "SeaDatabricksClient"),
42+
],
43+
)
44+
@pytest.mark.parametrize("proxy_uri", [None, "http://proxy.example.com:8081"])
45+
def test_python_backends_keep_http_client(
46+
self, backend_kwargs, backend_name, proxy_uri
47+
):
48+
with patch(f"{self.PACKAGE_NAME}.session.{backend_name}") as backend, patch(
49+
f"{self.PACKAGE_NAME}.client.UnifiedHttpClient"
50+
) as http_client, patch(
51+
f"{self.PACKAGE_NAME}.client.TelemetryClientFactory.get_telemetry_client"
52+
) as telemetry:
53+
http_client.return_value.using_proxy.return_value = proxy_uri is not None
54+
http_client.return_value.proxy_uri = proxy_uri
55+
conn = databricks.sql.connect(
56+
**self.DUMMY_CONNECTION_ARGS, **backend_kwargs
57+
)
58+
try:
59+
http_client.assert_called_once()
60+
assert conn.http_client is http_client.return_value
61+
assert conn.session.http_client is conn.http_client
62+
assert backend.call_args.kwargs["http_client"] is conn.http_client
63+
params = telemetry.return_value.export_initial_telemetry_log.call_args.kwargs[
64+
"driver_connection_params"
65+
]
66+
assert params.use_proxy is (proxy_uri is not None)
67+
assert params.use_system_proxy is (proxy_uri is not None)
68+
if proxy_uri:
69+
assert params.proxy_host_info.host_url == "proxy.example.com"
70+
assert params.proxy_host_info.port == 8081
71+
else:
72+
assert params.proxy_host_info is None
73+
finally:
74+
conn.close()
75+
backend.return_value.close_session.assert_called_once()
76+
http_client.return_value.close.assert_called_once()
77+
78+
def test_conflicting_kernel_and_sea_flags_preserve_error(self):
79+
with patch(
80+
f"{self.PACKAGE_NAME}.client.UnifiedHttpClient",
81+
side_effect=AssertionError(
82+
"Explicit kernel must not create a Python client"
83+
),
84+
) as http_client, pytest.raises(ValueError, match="mutually exclusive"):
85+
databricks.sql.connect(
86+
**self.DUMMY_CONNECTION_ARGS, use_kernel=True, use_sea=True
87+
)
88+
http_client.assert_not_called()
89+
3690
@patch("%s.session.ThriftDatabricksClient" % PACKAGE_NAME)
3791
def test_close_uses_the_correct_session_id(self, mock_client_class):
3892
instance = mock_client_class.return_value
@@ -877,6 +931,92 @@ def _connect(self, **overrides):
877931
args.update(overrides)
878932
return databricks.sql.connect(**args)
879933

934+
@pytest.mark.parametrize("route", ["explicit", "recovery", "cached"])
935+
@patch("%s.session.ThriftDatabricksClient" % PACKAGE)
936+
def test_kernel_http_client_lifecycle(self, mock_thrift, route):
937+
from databricks.sql.backend import reyden_warehouse_cache
938+
from databricks.sql.result_set import ResultSet
939+
940+
explicit = route == "explicit"
941+
mock_thrift.return_value.open_session.side_effect = self._reject()
942+
if route == "cached":
943+
reyden_warehouse_cache.mark_reyden(self.HOST, "wh-reyden")
944+
945+
with self._fake_kernel() as kernel, patch(
946+
f"{self.PACKAGE}.client.UnifiedHttpClient"
947+
) as http_client, patch(
948+
f"{self.PACKAGE}.client.TelemetryClientFactory.get_telemetry_client"
949+
) as telemetry:
950+
if explicit:
951+
http_client.side_effect = AssertionError(
952+
"Explicit kernel must not create a Python client"
953+
)
954+
http_client.return_value.using_proxy.return_value = True
955+
http_client.return_value.proxy_uri = "http://proxy.example.com:8081"
956+
result = Mock(spec=ResultSet, is_staging_operation=False)
957+
result.fetchall.return_value = [(1,)]
958+
kernel.return_value.execute_command.return_value = result
959+
conn = self._connect(
960+
**({"use_kernel": True} if explicit else {}),
961+
enable_telemetry=True,
962+
force_enable_telemetry=True,
963+
)
964+
try:
965+
assert conn.session.use_kernel is True
966+
assert conn.telemetry_enabled is False
967+
assert conn.http_client is (
968+
None if explicit else http_client.return_value
969+
)
970+
assert conn.session.http_client is conn.http_client
971+
assert kernel.call_args.kwargs["http_client"] is conn.http_client
972+
params = telemetry.return_value.export_initial_telemetry_log.call_args.kwargs[
973+
"driver_connection_params"
974+
]
975+
assert params.use_proxy is (not explicit)
976+
assert params.use_system_proxy is (not explicit)
977+
if explicit:
978+
assert params.proxy_host_info is None
979+
with conn.cursor() as cursor:
980+
cursor.execute("SELECT 1")
981+
assert cursor.fetchall() == [(1,)]
982+
kernel.return_value.execute_command.assert_called_once()
983+
finally:
984+
conn.close()
985+
kernel.return_value.close_session.assert_called_once()
986+
if explicit:
987+
http_client.assert_not_called()
988+
else:
989+
http_client.assert_called_once()
990+
http_client.return_value.close.assert_called_once()
991+
assert mock_thrift.return_value.open_session.call_count == (
992+
1 if route == "recovery" else 0
993+
)
994+
995+
@pytest.mark.parametrize("failure_stage", ["backend", "open"])
996+
def test_explicit_kernel_failure_preserves_error_and_telemetry(self, failure_stage):
997+
with self._fake_kernel() as kernel, patch(
998+
f"{self.PACKAGE}.client.UnifiedHttpClient",
999+
side_effect=AssertionError(
1000+
"Explicit kernel must not create a Python client"
1001+
),
1002+
) as http_client, patch(
1003+
f"{self.PACKAGE}.client.TelemetryClientFactory.connection_failure_log"
1004+
) as failure_log:
1005+
if failure_stage == "backend":
1006+
error = ImportError("kernel package unavailable")
1007+
kernel.side_effect = error
1008+
else:
1009+
error = OperationalError("kernel could not open session")
1010+
kernel.return_value.open_session.side_effect = error
1011+
with pytest.raises(type(error)) as excinfo:
1012+
self._connect(use_kernel=True, enable_telemetry=True)
1013+
assert excinfo.value is error
1014+
http_client.assert_not_called()
1015+
failure_log.assert_called_once()
1016+
assert failure_log.call_args.kwargs["enable_telemetry"] is (
1017+
failure_stage == "backend"
1018+
)
1019+
8801020
@pytest.mark.parametrize("route", ["explicit", "recovery", "cached"])
8811021
@pytest.mark.parametrize(
8821022
"geospatial_kwargs, expected",

0 commit comments

Comments
 (0)