Skip to content
Open
46 changes: 46 additions & 0 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
This module initializes the mssql_python package.
"""

import atexit
import sys
import threading
import types
import weakref
from typing import Dict

# Import settings from helpers to avoid circular imports
Expand Down Expand Up @@ -67,6 +70,49 @@
# Pooling
from .pooling import PoolingManager

# Global registry for tracking active connections (using weak references)
_active_connections = weakref.WeakSet()
_connections_lock = threading.Lock()


def _register_connection(conn):
"""Register a connection for cleanup before shutdown."""
with _connections_lock:
_active_connections.add(conn)


def _cleanup_connections():
"""
Cleanup function called by atexit to close all active connections.

This prevents resource leaks during interpreter shutdown by ensuring
all ODBC handles are freed in the correct order before Python finalizes.
"""
# Make a copy of the connections to avoid modification during iteration
with _connections_lock:
connections_to_close = list(_active_connections)

for conn in connections_to_close:
try:
# Check if connection is still valid and not closed
if hasattr(conn, "_closed") and not conn._closed:
# Close will handle both cursors and the connection
conn.close()
except Exception as e:
# Log errors during shutdown cleanup for debugging
# We're prioritizing crash prevention over error propagation
try:
driver_logger.error(
f"Error during connection cleanup at shutdown: {type(e).__name__}: {e}"
)
except Exception:
# If logging fails during shutdown, silently ignore
pass


# Register cleanup function to run before Python exits
atexit.register(_cleanup_connections)

# GLOBALS
# Read-Only
apilevel: str = "2.0"
Expand Down
17 changes: 17 additions & 0 deletions mssql_python/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Any, Dict, Optional, Union, List, Tuple, Callable, TYPE_CHECKING
import threading

import mssql_python
from mssql_python.cursor import Cursor
from mssql_python.helpers import (
add_driver_to_connection_str,
Expand Down Expand Up @@ -312,6 +313,22 @@ def __init__(
)
self.setautocommit(autocommit)

# Register this connection for cleanup before Python shutdown
# This ensures ODBC handles are freed in correct order, preventing leaks
try:
if hasattr(mssql_python, "_register_connection"):
mssql_python._register_connection(self)
except AttributeError as e:
# If registration fails, continue - cleanup will still happen via __del__
logger.warning(
f"Failed to register connection for shutdown cleanup: {type(e).__name__}: {e}"
)
except Exception as e:
# Catch any other unexpected errors during registration
logger.error(
f"Unexpected error during connection registration: {type(e).__name__}: {e}"
)

def _construct_connection_string(self, connection_str: str = "", **kwargs: Any) -> str:
"""
Construct the connection string by parsing, validating, and merging parameters.
Expand Down
14 changes: 9 additions & 5 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1163,11 +1163,15 @@ void SqlHandle::free() {
// Check if Python is shutting down using centralized helper function
bool pythonShuttingDown = is_python_finalizing();

// CRITICAL FIX: During Python shutdown, don't free STMT handles as
// their parent DBC may already be freed This prevents segfault when
// handles are freed in wrong order during interpreter shutdown Type 3 =
// SQL_HANDLE_STMT, Type 2 = SQL_HANDLE_DBC, Type 1 = SQL_HANDLE_ENV
if (pythonShuttingDown && _type == 3) {
// RESOURCE LEAK MITIGATION:
// When handles are skipped during shutdown, they are not freed, which could
// cause resource leaks. However, this is mitigated by:
// 1. Python-side atexit cleanup (in __init__.py) that explicitly closes all
// connections before shutdown, ensuring handles are freed in correct order
// 2. OS-level cleanup at process termination recovers any remaining resources
// 3. This tradeoff prioritizes crash prevention over resource cleanup, which
// is appropriate since we're already in shutdown sequence
if (pythonShuttingDown && (_type == SQL_HANDLE_STMT || _type == SQL_HANDLE_DBC)) {
_handle = nullptr; // Mark as freed to prevent double-free attempts
return;
}
Expand Down
Loading
Loading