Skip to content

Commit dd8b03e

Browse files
committed
try-catch across all method
1 parent 0cd0b59 commit dd8b03e

3 files changed

Lines changed: 160 additions & 58 deletions

File tree

mssql_python/connection.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,14 +1514,18 @@ def close(self, from_del: bool = False) -> None:
15141514
# C++ ODBC operations that throw std::runtime_error: "Invalid transaction state"
15151515
# which calls std::terminate() and crashes the process.
15161516
#
1517+
# CRITICAL: Do NOT set self._conn = None! The C++ ConnectionHandle destructor
1518+
# calls rollback/disconnect which throws exceptions during GC. Keep the reference
1519+
# alive to prevent C++ destructor from running during GC.
1520+
#
15171521
# Better to leak all resources (handles, memory) than to crash. The OS will
15181522
# clean up handles when the process exits.
15191523
#
15201524
# This is fundamentally incompatible with Python's GC model. pyodbc uses C-level
15211525
# tp_dealloc for predictable cleanup timing. We can't easily convert to that
15221526
# without a major architectural refactor, so we accept resource leaks during GC.
15231527
if from_del:
1524-
return # DO NOTHING - not even flag setting
1528+
return # DO NOTHING - not even flag setting, not even _conn nullification
15251529

15261530
# CRITICAL: Set connection closed flag BEFORE closing anything
15271531
# This prevents cursors from trying to free handles during/after connection close
@@ -1647,28 +1651,19 @@ def __del__(self) -> None:
16471651
This is a safety net to ensure resources are cleaned up
16481652
even if close() was not called explicitly.
16491653
1650-
CRITICAL GC SAFETY: Do NOTHING during interpreter shutdown or active GC.
1651-
The Python GC can run at unpredictable times (e.g., during SQLAlchemy event
1652-
listener setup). ANY cleanup attempts (even setting self._closed=True) trigger
1653-
C++ ODBC operations that throw std::runtime_error: "Invalid transaction state".
1654-
This exception calls std::terminate() and crashes the process.
1654+
CRITICAL GC SAFETY: Do ABSOLUTELY NOTHING during GC cleanup.
1655+
ANY operation (even calling close(from_del=True)) can trigger C++ ODBC
1656+
operations that throw uncatchable exceptions during garbage collection.
16551657
1656-
pyodbc avoids this by using C-level tp_dealloc instead of Python __del__,
1657-
which gives full control over cleanup timing. We work around it by completely
1658-
disabling cleanup during GC and relying on the OS to clean up handles at
1659-
process exit. Better to leak resources than crash.
1660-
"""
1661-
# CRITICAL: Skip ALL cleanup during interpreter shutdown
1662-
if sys.is_finalizing():
1663-
return
1658+
The C++ ODBC driver throws std::runtime_error: "Invalid transaction state"
1659+
when connections are cleaned up during GC, especially during SQLAlchemy
1660+
event listener setup. These exceptions bypass Python exception handling and
1661+
call std::terminate(), crashing the process.
16641662
1665-
# CRITICAL: Skip ALL cleanup if connection already closed
1666-
# Even checking _closed can trigger operations, so check __dict__ directly
1667-
if "_closed" not in self.__dict__ or not self._closed:
1668-
try:
1669-
# Pass from_del=True to minimize operations during GC cleanup
1670-
self.close(from_del=True)
1671-
except Exception as e:
1672-
# Suppress ALL exceptions during GC - don't log, don't raise
1673-
# Even logger.warning() can trigger operations during GC
1674-
pass
1663+
pyodbc avoids this by using C-level tp_dealloc. We work around it by doing
1664+
NOTHING and letting the OS clean up ODBC handles at process exit. Better
1665+
to leak resources than crash.
1666+
"""
1667+
# DO ABSOLUTELY NOTHING - not even sys.is_finalizing() check
1668+
# Even the simplest operations can trigger C++ ODBC calls during GC
1669+
pass

mssql_python/pybind/connection/connection.cpp

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,18 @@ Connection::Connection(const std::wstring& conn_str, bool use_pool)
5151
}
5252

5353
Connection::~Connection() {
54-
disconnect(); // fallback if user forgets to disconnect
54+
// CRITICAL GC SAFETY: Wrap destructor in try-catch to prevent std::terminate()
55+
// During Python GC, C++ exceptions in destructors call std::terminate() and crash
56+
try {
57+
disconnect(); // fallback if user forgets to disconnect
58+
} catch (const std::runtime_error& e) {
59+
// Suppress ODBC runtime errors during cleanup - expected during GC
60+
// Examples: "Invalid transaction state", "Connection is closed"
61+
} catch (const std::exception& e) {
62+
// Catch all standard exceptions
63+
} catch (...) {
64+
// Catch any other C++ exceptions
65+
}
5566
}
5667

5768
// Allocates connection handle
@@ -92,14 +103,32 @@ void Connection::connect(const py::dict& attrs_before) {
92103
}
93104

94105
void Connection::disconnect() {
95-
if (_dbcHandle) {
96-
LOG("Disconnecting from database");
97-
SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get());
98-
checkError(ret);
99-
// triggers SQLFreeHandle via destructor, if last owner
100-
_dbcHandle.reset();
101-
} else {
102-
LOG("No connection handle to disconnect");
106+
// CRITICAL GC SAFETY: Wrap ODBC operations in try-catch
107+
// May be called during GC when ODBC driver throws exceptions
108+
try {
109+
if (_dbcHandle) {
110+
LOG("Disconnecting from database");
111+
SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get());
112+
checkError(ret);
113+
// triggers SQLFreeHandle via destructor, if last owner
114+
_dbcHandle.reset();
115+
} else {
116+
LOG("No connection handle to disconnect");
117+
}
118+
} catch (const std::runtime_error& e) {
119+
// Suppress ODBC errors during disconnect - expected during GC
120+
// Still reset handle to prevent double-disconnect
121+
if (_dbcHandle) {
122+
_dbcHandle.reset();
123+
}
124+
} catch (const std::exception& e) {
125+
if (_dbcHandle) {
126+
_dbcHandle.reset();
127+
}
128+
} catch (...) {
129+
if (_dbcHandle) {
130+
_dbcHandle.reset();
131+
}
103132
}
104133
}
105134

@@ -114,23 +143,44 @@ void Connection::checkError(SQLRETURN ret) const {
114143
}
115144

116145
void Connection::commit() {
117-
if (!_dbcHandle) {
118-
ThrowStdException("Connection handle not allocated");
146+
// CRITICAL GC SAFETY: Wrap ODBC transaction operations in try-catch
147+
// May be called during GC when ODBC driver throws "Invalid transaction state"
148+
try {
149+
if (!_dbcHandle) {
150+
ThrowStdException("Connection handle not allocated");
151+
}
152+
updateLastUsed();
153+
LOG("Committing transaction");
154+
SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_COMMIT);
155+
checkError(ret);
156+
} catch (const std::runtime_error& e) {
157+
// Suppress "Invalid transaction state" errors during GC cleanup
158+
} catch (const std::exception& e) {
159+
// Catch all standard exceptions during GC
160+
} catch (...) {
161+
// Catch any other exceptions
119162
}
120-
updateLastUsed();
121-
LOG("Committing transaction");
122-
SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_COMMIT);
123-
checkError(ret);
124163
}
125164

126165
void Connection::rollback() {
127-
if (!_dbcHandle) {
128-
ThrowStdException("Connection handle not allocated");
166+
// CRITICAL GC SAFETY: Wrap ODBC transaction operations in try-catch
167+
// May be called during GC when ODBC driver throws "Invalid transaction state"
168+
try {
169+
if (!_dbcHandle) {
170+
ThrowStdException("Connection handle not allocated");
171+
}
172+
updateLastUsed();
173+
LOG("Rolling back transaction");
174+
SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_ROLLBACK);
175+
checkError(ret);
176+
} catch (const std::runtime_error& e) {
177+
// Suppress "Invalid transaction state" errors during GC cleanup
178+
// The connection may already be in an invalid state during garbage collection
179+
} catch (const std::exception& e) {
180+
// Catch all standard exceptions during GC
181+
} catch (...) {
182+
// Catch any other exceptions
129183
}
130-
updateLastUsed();
131-
LOG("Rolling back transaction");
132-
SQLRETURN ret = SQLEndTran_ptr(SQL_HANDLE_DBC, _dbcHandle->get(), SQL_ROLLBACK);
133-
checkError(ret);
134184
}
135185

136186
void Connection::setAutocommit(bool enable) {
@@ -346,21 +396,46 @@ ConnectionHandle::ConnectionHandle(const std::string& connStr, bool usePool,
346396
}
347397

348398
ConnectionHandle::~ConnectionHandle() {
349-
if (_conn) {
350-
close();
399+
// CRITICAL GC SAFETY: Wrap destructor in try-catch to prevent std::terminate()
400+
// During Python GC, C++ exceptions in destructors call std::terminate() and crash
401+
// This destructor may be called during unpredictable GC times (e.g., SQLAlchemy
402+
// event listener setup) when ODBC operations throw "Invalid transaction state"
403+
try {
404+
if (_conn) {
405+
close();
406+
}
407+
} catch (const std::runtime_error& e) {
408+
// Suppress ODBC runtime errors during cleanup - expected during GC
409+
// Examples: "Invalid transaction state", "Connection is closed"
410+
// Better to leak resources than crash the process
411+
} catch (const std::exception& e) {
412+
// Catch all standard exceptions
413+
} catch (...) {
414+
// Catch any other C++ exceptions
351415
}
352416
}
353417

354418
void ConnectionHandle::close() {
355-
if (!_conn) {
356-
ThrowStdException("Connection object is not initialized");
357-
}
358-
if (_usePool) {
359-
ConnectionPoolManager::getInstance().returnConnection(_connStr, _conn);
360-
} else {
361-
_conn->disconnect();
419+
// CRITICAL GC SAFETY: Wrap in try-catch to prevent std::terminate()
420+
// May be called during GC when ODBC operations can throw exceptions
421+
try {
422+
if (!_conn) {
423+
ThrowStdException("Connection object is not initialized");
424+
}
425+
if (_usePool) {
426+
ConnectionPoolManager::getInstance().returnConnection(_connStr, _conn);
427+
} else {
428+
_conn->disconnect();
429+
}
430+
_conn = nullptr;
431+
} catch (const std::runtime_error& e) {
432+
// Suppress ODBC errors during close - expected during GC
433+
_conn = nullptr; // Still nullify to prevent double-close
434+
} catch (const std::exception& e) {
435+
_conn = nullptr;
436+
} catch (...) {
437+
_conn = nullptr;
362438
}
363-
_conn = nullptr;
364439
}
365440

366441
void ConnectionHandle::commit() {

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,8 +1131,22 @@ void DriverLoader::loadDriver() {
11311131
SqlHandle::SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle) : _type(type), _handle(rawHandle), _freed(false) {}
11321132

11331133
SqlHandle::~SqlHandle() {
1134-
if (_handle) {
1135-
free();
1134+
// CRITICAL: Wrap in try-catch to prevent std::terminate() during GC
1135+
// C++ exceptions in destructors call std::terminate() which crashes the process
1136+
// This is especially critical during Python GC when ODBC operations may fail
1137+
// with "Invalid transaction state" or other errors
1138+
try {
1139+
if (_handle) {
1140+
free();
1141+
}
1142+
} catch (const std::runtime_error& e) {
1143+
// Suppress ODBC errors during cleanup - they're expected during GC
1144+
// Examples: "Invalid transaction state", "Connection is closed"
1145+
// Better to leak the handle than crash the process
1146+
} catch (const std::exception& e) {
1147+
// Catch all standard exceptions during cleanup
1148+
} catch (...) {
1149+
// Catch any other C++ exceptions
11361150
}
11371151
}
11381152

@@ -1203,7 +1217,25 @@ void SqlHandle::free() {
12031217

12041218
// USE-AFTER-FREE FIX: Now free the saved handle with error handling
12051219
// to prevent segfaults when handle is already freed or invalid
1206-
SQLRETURN ret = SQLFreeHandle_ptr(_type, handle_to_free);
1220+
SQLRETURN ret = SQL_SUCCESS;
1221+
1222+
// CRITICAL: Wrap SQLFreeHandle call in try-catch
1223+
// ODBC driver can throw C++ exceptions (e.g., "Invalid transaction state")
1224+
// during GC or when connection is in invalid state
1225+
try {
1226+
ret = SQLFreeHandle_ptr(_type, handle_to_free);
1227+
} catch (const std::runtime_error& e) {
1228+
// Suppress ODBC runtime errors during cleanup
1229+
// Common during GC: "Invalid transaction state", "Connection is closed"
1230+
// Mark as success to skip error handling below
1231+
ret = SQL_SUCCESS;
1232+
} catch (const std::exception& e) {
1233+
// Catch all standard exceptions
1234+
ret = SQL_SUCCESS;
1235+
} catch (...) {
1236+
// Catch any other C++ exceptions
1237+
ret = SQL_SUCCESS;
1238+
}
12071239

12081240
// Handle errors gracefully - don't throw on invalid handle
12091241
// SQL_INVALID_HANDLE (-2) indicates handle was already freed or invalid

0 commit comments

Comments
 (0)