Skip to content

Commit 15ccbab

Browse files
SNOW-2690900: isolate-server_session_keep_alive-condition (#2722)
1 parent 25202d4 commit 15ccbab

5 files changed

Lines changed: 131 additions & 23 deletions

File tree

DESCRIPTION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ https://docs.snowflake.com/
77
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python
88

99
# Release Notes
10+
- v4.2.1(TBD)
11+
- Made the parameter `server_session_keep_alive` in `SnowflakeConnection` skip checking for pending async queries, providing faster connection close times especially when many async queries are executed.
12+
1013
- v4.2.0(January 07,2026)
1114
- Added `SnowflakeCursor.stats` property to expose granular DML statistics (rows inserted, deleted, updated, and duplicates) for operations like CTAS where `rowcount` is insufficient.
1215
- Added support for injecting SPCS service identifier token (`SPCS_TOKEN`) into login requests when present in SPCS containers.

src/snowflake/connector/aio/_connection.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -930,22 +930,32 @@ async def close(self, retry: bool = True) -> None:
930930

931931
if self.telemetry_enabled:
932932
await self._telemetry.close(retry=retry)
933-
if (
934-
await self._all_async_queries_finished()
935-
and not self._server_session_keep_alive
936-
):
937-
logger.debug("No async queries seem to be running, deleting session")
938-
try:
939-
await self.rest.delete_session(retry=retry)
940-
except Exception as e:
933+
934+
if not self._server_session_keep_alive:
935+
if await self._all_async_queries_finished():
941936
logger.debug(
942-
"Exception encountered in deleting session. ignoring...: %s", e
937+
"No async queries seem to be running, deleting session"
943938
)
944-
else:
945-
logger.debug(
946-
"There are {} async queries still running, not deleting session".format(
947-
len(self._async_sfqids)
939+
try:
940+
await self.rest.delete_session(retry=retry)
941+
except Exception as e:
942+
logger.debug(
943+
"Exception encountered in deleting session. ignoring...: %s",
944+
e,
945+
)
946+
else:
947+
logger.debug(
948+
"There are {} async queries still running, not deleting session".format(
949+
len(self._async_sfqids)
950+
)
948951
)
952+
else:
953+
logger.info(
954+
"Parameter server_session_keep_alive was set to True - skipping session logout. "
955+
"If there are any not-finished queries in the current session (session_id: %s) - "
956+
"they will continue to live in Snowflake and consume credits until they finish. "
957+
"To cancel them use Monitoring tab in Snowsight or plain SQL.",
958+
self.session_id,
949959
)
950960
await self.rest.close()
951961
self._rest = None

src/snowflake/connector/connection.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,17 +1197,26 @@ def close(self, retry: bool = True) -> None:
11971197
logger.debug("closed")
11981198
if self.telemetry_enabled:
11991199
self._telemetry.close(retry=retry)
1200-
if (
1201-
self._all_async_queries_finished()
1202-
and not self._server_session_keep_alive
1203-
):
1204-
logger.debug("No async queries seem to be running, deleting session")
1205-
self.rest.delete_session(retry=retry)
1206-
else:
1207-
logger.debug(
1208-
"There are {} async queries still running, not deleting session".format(
1209-
len(self._async_sfqids)
1200+
1201+
if not self._server_session_keep_alive:
1202+
if self._all_async_queries_finished():
1203+
logger.debug(
1204+
"No async queries seem to be running, deleting session"
1205+
)
1206+
self.rest.delete_session(retry=retry)
1207+
else:
1208+
logger.debug(
1209+
"There are {} async queries still running, not deleting session".format(
1210+
len(self._async_sfqids)
1211+
)
12101212
)
1213+
else:
1214+
logger.info(
1215+
"Parameter server_session_keep_alive was set to True - skipping session logout. "
1216+
"If there are any not-finished queries in the current session (session_id: %s) - "
1217+
"they will continue to live in Snowflake and consume credits until they finish. "
1218+
"To cancel them use Monitoring tab in Snowsight or plain SQL.",
1219+
self.session_id,
12111220
)
12121221
self.rest.close()
12131222
self._rest = None

test/unit/aio/test_connection_async_unit.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,3 +934,47 @@ def test_connect_metadata_preservation():
934934
len(params) > 0
935935
), "connect should have parameters from SnowflakeConnection.__init__"
936936
# Should have parameters like account, user, password, etc.
937+
938+
939+
@pytest.mark.skipolddriver
940+
async def test_server_session_keep_alive_skips_async_check(mock_post_requests):
941+
"""Test that server_session_keep_alive=True skips _all_async_queries_finished check."""
942+
conn = fake_connector(server_session_keep_alive=True)
943+
await conn.connect()
944+
945+
# Mock the async methods we want to verify are called/not called
946+
conn._all_async_queries_finished = mock.AsyncMock(return_value=True)
947+
delete_session_mock = mock.AsyncMock()
948+
# rest attribute is deleted when closing the connection so accessing it in checks would fail
949+
conn.rest.delete_session = delete_session_mock
950+
951+
# Close the connection
952+
await conn.close()
953+
954+
# Verify _all_async_queries_finished was NOT called
955+
conn._all_async_queries_finished.assert_not_called()
956+
957+
# Verify delete_session was NOT called (due to server_session_keep_alive=True)
958+
delete_session_mock.assert_not_called()
959+
960+
961+
@pytest.mark.skipolddriver
962+
async def test_server_session_keep_alive_false_calls_async_check(mock_post_requests):
963+
"""Test that server_session_keep_alive=False calls _all_async_queries_finished check."""
964+
conn = fake_connector(server_session_keep_alive=False)
965+
await conn.connect()
966+
967+
# Mock the async methods we want to verify are called
968+
conn._all_async_queries_finished = mock.AsyncMock(return_value=True)
969+
delete_session_mock = mock.AsyncMock()
970+
# rest attribute is deleted when closing the connection so accessing it in checks would fail
971+
conn.rest.delete_session = delete_session_mock
972+
973+
# Close the connection
974+
await conn.close()
975+
976+
# Verify _all_async_queries_finished WAS called
977+
conn._all_async_queries_finished.assert_called_once()
978+
979+
# Verify delete_session WAS called (since async queries are finished and keep_alive=False)
980+
delete_session_mock.assert_called_once()

test/unit/test_connection.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,45 @@ def test_connections_registry_lifecycle(crl_mock, mock_post_requests):
980980
conn2.close()
981981
assert mock_registry.get_connection_count() == 0
982982
crl_mock.stop_periodic_cleanup.assert_called_once()
983+
984+
985+
@pytest.mark.skipolddriver
986+
def test_server_session_keep_alive_skips_async_check(mock_post_requests):
987+
"""Test that server_session_keep_alive=True skips _all_async_queries_finished check."""
988+
conn = fake_connector(server_session_keep_alive=True)
989+
990+
# Mock the methods we want to verify are called/not called
991+
conn._all_async_queries_finished = mock.MagicMock(return_value=True)
992+
delete_session_mock = mock.MagicMock()
993+
# rest attribute is deleted when closing the connection so accessing it in checks would fail
994+
conn.rest.delete_session = delete_session_mock
995+
996+
# Close the connection
997+
conn.close()
998+
999+
# Verify _all_async_queries_finished was NOT called
1000+
conn._all_async_queries_finished.assert_not_called()
1001+
1002+
# Verify delete_session was NOT called (due to server_session_keep_alive=True)
1003+
delete_session_mock.assert_not_called()
1004+
1005+
1006+
@pytest.mark.skipolddriver
1007+
def test_server_session_keep_alive_false_calls_async_check(mock_post_requests):
1008+
"""Test that server_session_keep_alive=False calls _all_async_queries_finished check."""
1009+
conn = fake_connector(server_session_keep_alive=False)
1010+
1011+
# Mock the methods we want to verify are called
1012+
conn._all_async_queries_finished = mock.MagicMock(return_value=True)
1013+
delete_session_mock = mock.MagicMock()
1014+
# rest attribute is deleted when closing the connection so accessing it in checks would fail
1015+
conn.rest.delete_session = delete_session_mock
1016+
1017+
# Close the connection
1018+
conn.close()
1019+
1020+
# Verify _all_async_queries_finished WAS called
1021+
conn._all_async_queries_finished.assert_called_once()
1022+
1023+
# Verify delete_session WAS called (since async queries are finished and keep_alive=False)
1024+
delete_session_mock.assert_called_once()

0 commit comments

Comments
 (0)