Skip to content

Commit e29be21

Browse files
authored
Merge pull request #54 from databricks/PECOBLR-1655-do-ping
[PECOBLR-1655]Override do_ping for pool_pre_ping support
2 parents 6b80531 + da7c92b commit e29be21

7 files changed

Lines changed: 116 additions & 0 deletions

File tree

.github/workflows/code-quality-checks.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
- name: Install Poetry
3131
uses: snok/install-poetry@v1
3232
with:
33+
version: "2.2.1"
3334
virtualenvs-create: true
3435
virtualenvs-in-project: true
3536
installer-parallel: true
@@ -82,6 +83,7 @@ jobs:
8283
- name: Install Poetry
8384
uses: snok/install-poetry@v1
8485
with:
86+
version: "2.2.1"
8587
virtualenvs-create: true
8688
virtualenvs-in-project: true
8789
installer-parallel: true

.github/workflows/integration.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
- name: Install Poetry
3434
uses: snok/install-poetry@v1
3535
with:
36+
version: "2.2.1"
3637
virtualenvs-create: true
3738
virtualenvs-in-project: true
3839
installer-parallel: true

.github/workflows/publish-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- name: Install Poetry
2222
uses: snok/install-poetry@v1
2323
with:
24+
version: "2.2.1"
2425
virtualenvs-create: true
2526
virtualenvs-in-project: true
2627
installer-parallel: true

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- name: Install Poetry
2525
uses: snok/install-poetry@v1
2626
with:
27+
version: "2.2.1"
2728
virtualenvs-create: true
2829
virtualenvs-in-project: true
2930
installer-parallel: true

src/databricks/sqlalchemy/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,25 @@ def do_rollback(self, dbapi_connection):
336336
# Databricks SQL Does not support transactions
337337
pass
338338

339+
def do_ping(self, dbapi_connection):
340+
"""Check if the connection is usable.
341+
342+
Called by SQLAlchemy when pool_pre_ping=True before checking out
343+
a connection from the pool. If this returns False, the connection
344+
is invalidated and a new one is created.
345+
346+
Any error during the ping means the connection is unusable
347+
"""
348+
try:
349+
cursor = dbapi_connection.cursor()
350+
try:
351+
cursor.execute("SELECT 1")
352+
finally:
353+
cursor.close()
354+
return True
355+
except Exception:
356+
return False
357+
339358
@reflection.cache
340359
def has_table(
341360
self, connection, table_name, schema=None, catalog=None, **kwargs

tests/test_local/e2e/test_basic.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,50 @@ def test_table_comment_reflection(self, inspector: Inspector, table: Table):
541541
def test_column_comment(self, inspector: Inspector, table: Table):
542542
result = inspector.get_columns(table.name)[0].get("comment")
543543
assert result == "column comment"
544+
545+
546+
def test_pool_pre_ping_with_closed_connection(connection_details):
547+
"""Test that pool_pre_ping detects closed connections and creates new ones.
548+
549+
When a pooled connection is closed (simulating session expiration),
550+
do_ping() detects it and SQLAlchemy creates a new connection.
551+
"""
552+
conn_string, connect_args = version_agnostic_connect_arguments(connection_details)
553+
554+
engine = create_engine(
555+
conn_string,
556+
connect_args=connect_args,
557+
pool_pre_ping=True,
558+
pool_size=1,
559+
max_overflow=0,
560+
)
561+
562+
# Step 1: Use a connection and record its session ID
563+
with engine.connect() as conn:
564+
result = conn.execute(text("SELECT VERSION()")).scalar()
565+
assert result is not None
566+
567+
raw_conn = conn.connection.dbapi_connection
568+
session_id_1 = raw_conn.get_session_id_hex()
569+
assert session_id_1 is not None
570+
571+
# Step 2: Close the pooled connection to simulate session expiration
572+
pooled_conn = engine.pool._pool.queue[0]
573+
pooled_conn.driver_connection.close()
574+
assert not pooled_conn.driver_connection.open
575+
576+
# Step 3: pool_pre_ping should detect the dead connection and create a new one
577+
with engine.connect() as conn:
578+
result = conn.execute(text("SELECT VERSION()")).scalar()
579+
assert result is not None
580+
581+
raw_conn = conn.connection.dbapi_connection
582+
session_id_2 = raw_conn.get_session_id_hex()
583+
assert session_id_2 is not None
584+
585+
assert session_id_1 != session_id_2, (
586+
"pool_pre_ping should have detected the closed connection "
587+
"and created a new one with a different session ID"
588+
)
589+
590+
engine.dispose()

tests/test_local/test_do_ping.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for DatabricksDialect.do_ping() method."""
2+
from unittest.mock import MagicMock, patch
3+
import pytest
4+
from databricks.sqlalchemy import DatabricksDialect
5+
6+
7+
class TestDoPing:
8+
@pytest.fixture
9+
def dialect(self):
10+
return DatabricksDialect()
11+
12+
def test_ping_success(self, dialect):
13+
"""do_ping returns True when SELECT 1 succeeds."""
14+
mock_conn = MagicMock()
15+
assert dialect.do_ping(mock_conn) is True
16+
mock_conn.cursor.assert_called_once()
17+
mock_conn.cursor().execute.assert_called_once_with("SELECT 1")
18+
mock_conn.cursor().close.assert_called_once()
19+
20+
def test_ping_cursor_fails(self, dialect):
21+
"""do_ping returns False when cursor() raises (connection closed)."""
22+
mock_conn = MagicMock()
23+
mock_conn.cursor.side_effect = Exception("Cannot create cursor from closed connection")
24+
assert dialect.do_ping(mock_conn) is False
25+
26+
def test_ping_execute_fails(self, dialect):
27+
"""do_ping returns False when execute() raises (session expired)."""
28+
mock_conn = MagicMock()
29+
mock_conn.cursor().execute.side_effect = Exception("Invalid SessionHandle")
30+
assert dialect.do_ping(mock_conn) is False
31+
32+
def test_ping_cursor_closed_on_success(self, dialect):
33+
"""Cursor is closed after a successful ping."""
34+
mock_conn = MagicMock()
35+
dialect.do_ping(mock_conn)
36+
mock_conn.cursor().close.assert_called_once()
37+
38+
def test_ping_cursor_closed_on_execute_failure(self, dialect):
39+
"""Cursor is closed even when execute() fails."""
40+
mock_conn = MagicMock()
41+
mock_cursor = MagicMock()
42+
mock_conn.cursor.return_value = mock_cursor
43+
mock_cursor.execute.side_effect = Exception("network error")
44+
dialect.do_ping(mock_conn)
45+
mock_cursor.close.assert_called_once()

0 commit comments

Comments
 (0)