diff --git a/DESCRIPTION.md b/DESCRIPTION.md index c49575c4d..4f0a7f26f 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -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. diff --git a/src/snowflake/connector/aio/_connection.py b/src/snowflake/connector/aio/_connection.py index 288e0d5ca..09e169049 100644 --- a/src/snowflake/connector/aio/_connection.py +++ b/src/snowflake/connector/aio/_connection.py @@ -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 diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index 533379ddb..d46d66adf 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -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( + "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 diff --git a/test/unit/aio/test_connection_async_unit.py b/test/unit/aio/test_connection_async_unit.py index d3bfe489b..d4943d60e 100644 --- a/test/unit/aio/test_connection_async_unit.py +++ b/test/unit/aio/test_connection_async_unit.py @@ -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() diff --git a/test/unit/test_connection.py b/test/unit/test_connection.py index 94474de22..827fbac65 100644 --- a/test/unit/test_connection.py +++ b/test/unit/test_connection.py @@ -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()