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

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

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

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


def _register_connection(conn):
"""Register a connection for cleanup before shutdown."""
_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
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:
# Silently ignore errors during shutdown cleanup
# We're prioritizing crash prevention over error reporting
pass


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

# GLOBALS
# Read-Only
apilevel: str = "2.0"
Expand Down
11 changes: 11 additions & 0 deletions mssql_python/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,17 @@ 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:
import mssql_python

if hasattr(mssql_python, "_register_connection"):
mssql_python._register_connection(self)
except (ImportError, AttributeError):
# If registration fails, continue - cleanup will still happen via __del__
pass

def _construct_connection_string(self, connection_str: str = "", **kwargs: Any) -> str:
"""
Construct the connection string by parsing, validating, and merging parameters.
Expand Down
21 changes: 16 additions & 5 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1124,11 +1124,22 @@ 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) {
// CRITICAL FIX: During Python shutdown, don't free STMT or DBC handles as
// their parent handles may already be freed. This prevents segfaults when
// handles are freed in the wrong order during interpreter shutdown.
// Type 3 = SQL_HANDLE_STMT (parent: DBC)
// Type 2 = SQL_HANDLE_DBC (parent: ENV, which is static and may destruct first)
// Type 1 = SQL_HANDLE_ENV (no parent)
//
// 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 == 3 || _type == 2)) {
_handle = nullptr; // Mark as freed to prevent double-free attempts
return;
}
Expand Down
Loading
Loading