Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ https://docs.snowflake.com/
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python

# Release Notes
- v4.2.1(TBD)
- 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.

- v4.2.0(December 17,2025)
- Added `SnowflakeCursor.stats` property to expose granular DML statistics (rows inserted, deleted, updated, and duplicates) for operations like CTAS where `rowcount` is insufficient.
- Added support for injecting SPCS service identifier token (`SPCS_TOKEN`) into login requests when present in SPCS containers.
Expand Down
36 changes: 23 additions & 13 deletions src/snowflake/connector/aio/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,22 +930,32 @@ async def close(self, retry: bool = True) -> None:

if self.telemetry_enabled:
await self._telemetry.close(retry=retry)
if (
await self._all_async_queries_finished()
and not self._server_session_keep_alive
):
logger.debug("No async queries seem to be running, deleting session")
try:
await self.rest.delete_session(retry=retry)
except Exception as e:

if not self._server_session_keep_alive:
if await self._all_async_queries_finished():
logger.debug(
"Exception encountered in deleting session. ignoring...: %s", e
"No async queries seem to be running, deleting session"
)
else:
logger.debug(
"There are {} async queries still running, not deleting session".format(
len(self._async_sfqids)
try:
await self.rest.delete_session(retry=retry)
except Exception as e:
logger.debug(
"Exception encountered in deleting session. ignoring...: %s",
e,
)
else:
logger.debug(
"There are {} async queries still running, not deleting session".format(
len(self._async_sfqids)
)
)
else:
logger.info(
"Parameter server_session_keep_alive was set to True - skipping session logout. "
"If there are any not-finished queries in the current session (session_id: %s) - "
"they will continue to live in Snowflake and consume credits until they finish. "
"To cancel them use Monitoring tab in Snowsight or plain SQL.",
self.session_id,
)
await self.rest.close()
self._rest = None
Expand Down
29 changes: 19 additions & 10 deletions src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,17 +1197,26 @@ def close(self, retry: bool = True) -> None:
logger.debug("closed")
if self.telemetry_enabled:
self._telemetry.close(retry=retry)
if (
self._all_async_queries_finished()
and not self._server_session_keep_alive
):
logger.debug("No async queries seem to be running, deleting session")
self.rest.delete_session(retry=retry)
else:
logger.debug(
"There are {} async queries still running, not deleting session".format(
len(self._async_sfqids)

if not self._server_session_keep_alive:
if self._all_async_queries_finished():
logger.debug(
"No async queries seem to be running, deleting session"
)
self.rest.delete_session(retry=retry)
else:
logger.debug(
"There are {} async queries still running, not deleting session".format(
len(self._async_sfqids)
)
)
else:
logger.info(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important to make sure such communication at the INFO level is smth we want.

"Parameter server_session_keep_alive was set to True - skipping session logout. "
"If there are any not-finished queries in the current session (session_id: %s) - "
"they will continue to live in Snowflake and consume credits until they finish. "
"To cancel them use Monitoring tab in Snowsight or plain SQL.",
self.session_id,
)
self.rest.close()
self._rest = None
Expand Down
44 changes: 44 additions & 0 deletions test/unit/aio/test_connection_async_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,3 +934,47 @@ def test_connect_metadata_preservation():
len(params) > 0
), "connect should have parameters from SnowflakeConnection.__init__"
# Should have parameters like account, user, password, etc.


@pytest.mark.skipolddriver
async def test_server_session_keep_alive_skips_async_check(mock_post_requests):
"""Test that server_session_keep_alive=True skips _all_async_queries_finished check."""
conn = fake_connector(server_session_keep_alive=True)
await conn.connect()

# Mock the async methods we want to verify are called/not called
conn._all_async_queries_finished = mock.AsyncMock(return_value=True)
delete_session_mock = mock.AsyncMock()
# rest attribute is deleted when closing the connection so accessing it in checks would fail
conn.rest.delete_session = delete_session_mock

# Close the connection
await conn.close()

# Verify _all_async_queries_finished was NOT called
conn._all_async_queries_finished.assert_not_called()

# Verify delete_session was NOT called (due to server_session_keep_alive=True)
delete_session_mock.assert_not_called()


@pytest.mark.skipolddriver
async def test_server_session_keep_alive_false_calls_async_check(mock_post_requests):
"""Test that server_session_keep_alive=False calls _all_async_queries_finished check."""
conn = fake_connector(server_session_keep_alive=False)
await conn.connect()

# Mock the async methods we want to verify are called
conn._all_async_queries_finished = mock.AsyncMock(return_value=True)
delete_session_mock = mock.AsyncMock()
# rest attribute is deleted when closing the connection so accessing it in checks would fail
conn.rest.delete_session = delete_session_mock

# Close the connection
await conn.close()

# Verify _all_async_queries_finished WAS called
conn._all_async_queries_finished.assert_called_once()

# Verify delete_session WAS called (since async queries are finished and keep_alive=False)
delete_session_mock.assert_called_once()
42 changes: 42 additions & 0 deletions test/unit/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,3 +980,45 @@ def test_connections_registry_lifecycle(crl_mock, mock_post_requests):
conn2.close()
assert mock_registry.get_connection_count() == 0
crl_mock.stop_periodic_cleanup.assert_called_once()


@pytest.mark.skipolddriver
def test_server_session_keep_alive_skips_async_check(mock_post_requests):
"""Test that server_session_keep_alive=True skips _all_async_queries_finished check."""
conn = fake_connector(server_session_keep_alive=True)

# Mock the methods we want to verify are called/not called
conn._all_async_queries_finished = mock.MagicMock(return_value=True)
delete_session_mock = mock.MagicMock()
# rest attribute is deleted when closing the connection so accessing it in checks would fail
conn.rest.delete_session = delete_session_mock

# Close the connection
conn.close()

# Verify _all_async_queries_finished was NOT called
conn._all_async_queries_finished.assert_not_called()

# Verify delete_session was NOT called (due to server_session_keep_alive=True)
delete_session_mock.assert_not_called()


@pytest.mark.skipolddriver
def test_server_session_keep_alive_false_calls_async_check(mock_post_requests):
"""Test that server_session_keep_alive=False calls _all_async_queries_finished check."""
conn = fake_connector(server_session_keep_alive=False)

# Mock the methods we want to verify are called
conn._all_async_queries_finished = mock.MagicMock(return_value=True)
delete_session_mock = mock.MagicMock()
# rest attribute is deleted when closing the connection so accessing it in checks would fail
conn.rest.delete_session = delete_session_mock

# Close the connection
conn.close()

# Verify _all_async_queries_finished WAS called
conn._all_async_queries_finished.assert_called_once()

# Verify delete_session WAS called (since async queries are finished and keep_alive=False)
delete_session_mock.assert_called_once()
Loading