Skip to content

Commit 9c132c2

Browse files
NO-SNOW: Stop CRL cache background task when closing conn (#2633)
1 parent a2162d8 commit 9c132c2

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

src/snowflake/connector/connection.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
)
8989
from .converter import SnowflakeConverter
9090
from .crl import CRLConfig
91+
from .crl_cache import CRLCacheFactory
9192
from .cursor import LOG_MAX_QUERY_LENGTH, SnowflakeCursor, SnowflakeCursorBase
9293
from .description import (
9394
CLIENT_NAME,
@@ -1152,11 +1153,17 @@ def connect(self, **kwargs) -> None:
11521153
else:
11531154
self.__open_connection()
11541155

1156+
# Register the connection in the pool after successful connection
1157+
_connections_registry.add_connection(self)
1158+
11551159
def close(self, retry: bool = True) -> None:
11561160
"""Closes the connection."""
11571161
# unregister to dereference connection object as it's already closed after the execution
11581162
atexit.unregister(self._close_at_exit)
11591163
try:
1164+
# Remove connection from the pool
1165+
_connections_registry.remove_connection(self)
1166+
11601167
if not self.rest:
11611168
logger.debug("Rest object has been destroyed, cannot close session")
11621169
return
@@ -2533,3 +2540,56 @@ def _detect_application() -> None | str:
25332540
return "jupyter_notebook"
25342541
if "snowbooks" in sys.modules:
25352542
return "snowflake_notebook"
2543+
2544+
2545+
class _ConnectionsRegistry:
2546+
"""Thread-safe registry for tracking opened SnowflakeConnection instances.
2547+
2548+
This class maintains a registry of active connections using weak references
2549+
to avoid preventing garbage collection.
2550+
"""
2551+
2552+
def __init__(self):
2553+
"""Initialize the connections registry with an empty registry and a lock."""
2554+
self._connections: weakref.WeakSet = weakref.WeakSet()
2555+
self._lock = Lock()
2556+
2557+
def add_connection(self, connection: SnowflakeConnection) -> None:
2558+
"""Add a connection to the registry.
2559+
2560+
Args:
2561+
connection: The SnowflakeConnection instance to register.
2562+
"""
2563+
with self._lock:
2564+
self._connections.add(connection)
2565+
logger.debug(
2566+
f"Connection {id(connection)} added to pool. Total connections: {len(self._connections)}"
2567+
)
2568+
2569+
def remove_connection(self, connection: SnowflakeConnection) -> None:
2570+
"""Remove a connection from the registry.
2571+
2572+
Args:
2573+
connection: The SnowflakeConnection instance to unregister.
2574+
"""
2575+
with self._lock:
2576+
self._connections.discard(connection)
2577+
logger.debug(
2578+
f"Connection {id(connection)} removed from registry. Total connections: {len(self._connections)}"
2579+
)
2580+
2581+
if len(self._connections) == 0:
2582+
self._last_connection_handler()
2583+
2584+
def _last_connection_handler(self):
2585+
# If no connections left then stop CRL background task
2586+
# to avoid script dangling
2587+
CRLCacheFactory.stop_periodic_cleanup()
2588+
2589+
def get_connection_count(self) -> int:
2590+
with self._lock:
2591+
return len(self._connections)
2592+
2593+
2594+
# Global instance of the connections pool
2595+
_connections_registry = _ConnectionsRegistry()

src/snowflake/connector/crl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class CRLConfig:
6363
crl_cache_dir: Path | str | None = None
6464
crl_cache_removal_delay_days: int = 7
6565
crl_cache_cleanup_interval_hours: int = 1
66-
crl_cache_start_cleanup: bool = False
66+
crl_cache_start_cleanup: bool = True
6767

6868
@classmethod
6969
def from_connection(cls, sf_connection) -> CRLConfig:

test/unit/test_connection.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,3 +953,28 @@ def test_connect_metadata_preservation():
953953
len(params) > 0
954954
), "connect should have parameters from SnowflakeConnection.__init__"
955955
# Should have parameters like account, user, password, etc.
956+
957+
958+
@mock.patch("snowflake.connector.connection.CRLCacheFactory")
959+
def test_connections_registry_lifecycle(crl_mock, mock_post_requests):
960+
"""Test the individual methods of _ConnectionsPool."""
961+
from snowflake.connector.connection import _ConnectionsRegistry
962+
963+
# Mock the registry to avoid side effects from other tests due to _ConnectionsRegistry being a singleton
964+
with mock.patch(
965+
"snowflake.connector.connection._connections_registry", _ConnectionsRegistry()
966+
) as mock_registry:
967+
# Create a connection
968+
conn1 = fake_connector()
969+
conn2 = fake_connector()
970+
assert mock_registry.get_connection_count() == 2
971+
972+
# Don't stop the task if pool is not empty
973+
conn1.close()
974+
crl_mock.stop_periodic_cleanup.assert_not_called()
975+
assert mock_registry.get_connection_count() == 1
976+
977+
# Stop the task if the pool is emptied
978+
conn2.close()
979+
assert mock_registry.get_connection_count() == 0
980+
crl_mock.stop_periodic_cleanup.assert_called_once()

0 commit comments

Comments
 (0)