Skip to content

Commit 3fa8931

Browse files
saurabh500Copilot
andcommitted
Release GIL during blocking ODBC connect/disconnect calls
Release the Python GIL during SQLDriverConnect and SQLDisconnect calls so that other Python threads can run concurrently during connection I/O (DNS resolution, TCP handshake, TLS negotiation, SQL Server auth). Changes: - connection.cpp: Wrap SQLDriverConnect_ptr in py::gil_scoped_release - connection.cpp: Conditionally release GIL around SQLDisconnect_ptr (guarded by PyGILState_Check for destructor safety) - connection.cpp: Remove disconnect() from reset() failure paths to avoid GIL release inside pool mutex; callers handle cleanup - connection_pool.cpp: Move connect() outside pool mutex in acquire() (reserve slot inside mutex, connect outside, rollback on failure) - connection_pool.cpp: Move disconnect() outside mutex in release() - connection_pool.cpp: Release _manager_mutex before calling pool acquire/release/close to prevent mutex/GIL lock-ordering deadlocks - Add test_021_concurrent_connection_perf.py with stress tests proving concurrent connection parallelism and no GIL starvation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2398ffd commit 3fa8931

File tree

3 files changed

+371
-28
lines changed

3 files changed

+371
-28
lines changed

mssql_python/pybind/connection/connection.cpp

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,16 @@ void Connection::connect(const py::dict& attrs_before) {
8585
#else
8686
connStrPtr = const_cast<SQLWCHAR*>(_connStr.c_str());
8787
#endif
88-
SQLRETURN ret = SQLDriverConnect_ptr(_dbcHandle->get(), nullptr, connStrPtr, SQL_NTS, nullptr,
89-
0, nullptr, SQL_DRIVER_NOPROMPT);
88+
SQLRETURN ret;
89+
{
90+
// Release the GIL during the blocking ODBC connect call.
91+
// SQLDriverConnect involves DNS resolution, TCP handshake, TLS negotiation,
92+
// and SQL Server authentication — all pure I/O that doesn't need the GIL.
93+
// This allows other Python threads to run concurrently.
94+
py::gil_scoped_release release;
95+
ret = SQLDriverConnect_ptr(_dbcHandle->get(), nullptr, connStrPtr, SQL_NTS, nullptr,
96+
0, nullptr, SQL_DRIVER_NOPROMPT);
97+
}
9098
checkError(ret);
9199
updateLastUsed();
92100
}
@@ -95,6 +103,11 @@ void Connection::disconnect() {
95103
if (_dbcHandle) {
96104
LOG("Disconnecting from database");
97105

106+
// Check if we hold the GIL so we can conditionally release it.
107+
// The GIL is held when called from pybind11-bound methods but may NOT
108+
// be held in destructor paths (C++ shared_ptr ref-count drop, shutdown).
109+
bool hasGil = PyGILState_Check() != 0;
110+
98111
// CRITICAL FIX: Mark all child statement handles as implicitly freed
99112
// When we free the DBC handle below, the ODBC driver will automatically free
100113
// all child STMT handles. We need to tell the SqlHandle objects about this
@@ -135,7 +148,17 @@ void Connection::disconnect() {
135148
_allocationsSinceCompaction = 0;
136149
} // Release lock before potentially slow SQLDisconnect call
137150

138-
SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get());
151+
SQLRETURN ret;
152+
if (hasGil) {
153+
// Release the GIL during the blocking ODBC disconnect call.
154+
// This allows other Python threads to run while the network
155+
// round-trip completes.
156+
py::gil_scoped_release release;
157+
ret = SQLDisconnect_ptr(_dbcHandle->get());
158+
} else {
159+
// Destructor / shutdown path — GIL is not held, call directly.
160+
ret = SQLDisconnect_ptr(_dbcHandle->get());
161+
}
139162
checkError(ret);
140163
// triggers SQLFreeHandle via destructor, if last owner
141164
_dbcHandle.reset();
@@ -375,7 +398,6 @@ bool Connection::reset() {
375398
(SQLPOINTER)SQL_RESET_CONNECTION_YES, SQL_IS_INTEGER);
376399
if (!SQL_SUCCEEDED(ret)) {
377400
LOG("Failed to reset connection (ret=%d). Marking as dead.", ret);
378-
disconnect();
379401
return false;
380402
}
381403

@@ -387,7 +409,6 @@ bool Connection::reset() {
387409
(SQLPOINTER)SQL_TXN_READ_COMMITTED, SQL_IS_INTEGER);
388410
if (!SQL_SUCCEEDED(ret)) {
389411
LOG("Failed to reset transaction isolation level (ret=%d). Marking as dead.", ret);
390-
disconnect();
391412
return false;
392413
}
393414

mssql_python/pybind/connection/connection_pool.cpp

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ std::shared_ptr<Connection> ConnectionPool::acquire(const std::wstring& connStr,
1616
const py::dict& attrs_before) {
1717
std::vector<std::shared_ptr<Connection>> to_disconnect;
1818
std::shared_ptr<Connection> valid_conn = nullptr;
19+
bool needs_connect = false;
1920
{
2021
std::lock_guard<std::mutex> lock(_mutex);
2122
auto now = std::chrono::steady_clock::now();
@@ -57,16 +58,33 @@ std::shared_ptr<Connection> ConnectionPool::acquire(const std::wstring& connStr,
5758
}
5859
}
5960

60-
// Create new connection if none reusable
61+
// Reserve a slot for a new connection if none reusable.
62+
// The actual connect() call happens outside the mutex to avoid
63+
// holding the mutex during the blocking ODBC call (which releases
64+
// the GIL and could otherwise cause a mutex/GIL deadlock).
6165
if (!valid_conn && _current_size < _max_size) {
6266
valid_conn = std::make_shared<Connection>(connStr, true);
63-
valid_conn->connect(attrs_before);
6467
++_current_size;
68+
needs_connect = true;
6569
} else if (!valid_conn) {
6670
throw std::runtime_error("ConnectionPool::acquire: pool size limit reached");
6771
}
6872
}
6973

74+
// Phase 2.5: Connect the new connection outside the mutex.
75+
if (needs_connect) {
76+
try {
77+
valid_conn->connect(attrs_before);
78+
} catch (...) {
79+
// Connect failed — release the reserved slot
80+
{
81+
std::lock_guard<std::mutex> lock(_mutex);
82+
if (_current_size > 0) --_current_size;
83+
}
84+
throw;
85+
}
86+
}
87+
7088
// Phase 3: Disconnect expired/bad connections outside lock
7189
for (auto& conn : to_disconnect) {
7290
try {
@@ -79,14 +97,22 @@ std::shared_ptr<Connection> ConnectionPool::acquire(const std::wstring& connStr,
7997
}
8098

8199
void ConnectionPool::release(std::shared_ptr<Connection> conn) {
82-
std::lock_guard<std::mutex> lock(_mutex);
83-
if (_pool.size() < _max_size) {
84-
conn->updateLastUsed();
85-
_pool.push_back(conn);
86-
} else {
100+
bool should_disconnect = false;
101+
{
102+
std::lock_guard<std::mutex> lock(_mutex);
103+
if (_pool.size() < _max_size) {
104+
conn->updateLastUsed();
105+
_pool.push_back(conn);
106+
} else {
107+
should_disconnect = true;
108+
if (_current_size > 0)
109+
--_current_size;
110+
}
111+
}
112+
// Disconnect outside the mutex to avoid holding it during the
113+
// blocking ODBC call (which releases the GIL).
114+
if (should_disconnect) {
87115
conn->disconnect();
88-
if (_current_size > 0)
89-
--_current_size;
90116
}
91117
}
92118

@@ -116,21 +142,35 @@ ConnectionPoolManager& ConnectionPoolManager::getInstance() {
116142

117143
std::shared_ptr<Connection> ConnectionPoolManager::acquireConnection(const std::wstring& connStr,
118144
const py::dict& attrs_before) {
119-
std::lock_guard<std::mutex> lock(_manager_mutex);
120-
121-
auto& pool = _pools[connStr];
122-
if (!pool) {
123-
LOG("Creating new connection pool");
124-
pool = std::make_shared<ConnectionPool>(_default_max_size, _default_idle_secs);
145+
std::shared_ptr<ConnectionPool> pool;
146+
{
147+
std::lock_guard<std::mutex> lock(_manager_mutex);
148+
auto& pool_ref = _pools[connStr];
149+
if (!pool_ref) {
150+
LOG("Creating new connection pool");
151+
pool_ref = std::make_shared<ConnectionPool>(_default_max_size, _default_idle_secs);
152+
}
153+
pool = pool_ref;
125154
}
155+
// Call acquire() outside _manager_mutex. acquire() may release the GIL
156+
// during the ODBC connect call; holding _manager_mutex across that would
157+
// create a mutex/GIL lock-ordering deadlock.
126158
return pool->acquire(connStr, attrs_before);
127159
}
128160

129161
void ConnectionPoolManager::returnConnection(const std::wstring& conn_str,
130162
const std::shared_ptr<Connection> conn) {
131-
std::lock_guard<std::mutex> lock(_manager_mutex);
132-
if (_pools.find(conn_str) != _pools.end()) {
133-
_pools[conn_str]->release((conn));
163+
std::shared_ptr<ConnectionPool> pool;
164+
{
165+
std::lock_guard<std::mutex> lock(_manager_mutex);
166+
auto it = _pools.find(conn_str);
167+
if (it != _pools.end()) {
168+
pool = it->second;
169+
}
170+
}
171+
// Call release() outside _manager_mutex to avoid deadlock.
172+
if (pool) {
173+
pool->release(conn);
134174
}
135175
}
136176

@@ -141,11 +181,18 @@ void ConnectionPoolManager::configure(int max_size, int idle_timeout_secs) {
141181
}
142182

143183
void ConnectionPoolManager::closePools() {
144-
std::lock_guard<std::mutex> lock(_manager_mutex);
145-
for (auto& [conn_str, pool] : _pools) {
146-
if (pool) {
147-
pool->close();
184+
std::vector<std::shared_ptr<ConnectionPool>> pools_to_close;
185+
{
186+
std::lock_guard<std::mutex> lock(_manager_mutex);
187+
for (auto& [conn_str, pool] : _pools) {
188+
if (pool) {
189+
pools_to_close.push_back(pool);
190+
}
148191
}
192+
_pools.clear();
193+
}
194+
// Close pools outside _manager_mutex to avoid deadlock.
195+
for (auto& pool : pools_to_close) {
196+
pool->close();
149197
}
150-
_pools.clear();
151198
}

0 commit comments

Comments
 (0)