Skip to content

FIX: Bound pooled connection checkout on unresponsive sockets - #859

Open
Jahnvi Thakkar (jahnvi480) wants to merge 3 commits into
mainfrom
jahnvi/pooled-checkout-timeout
Open

Jahnvi Thakkar (jahnvi480) wants to merge 3 commits into
mainfrom
jahnvi/pooled-checkout-timeout

Conversation

@jahnvi480

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

GitHub Issue: #854


Summary

Bound pooled connection reset and autocommit configuration using a temporary native connection timeout. Use the positive login timeout (explicit attrs_before takes precedence), or a 15-second validation default, and restore the previous connection timeout on success. After validation fails, open a replacement rather than probing every stale idle socket sequentially. Preserve isolation/transaction cleanup and document that cancellation, cleanup, and replacement login can add time: this is not an end-to-end connect deadline.

Add stalled-socket regressions covering both autocommit modes, one/three stale sockets, timeout precedence, the finite default, and timeout restoration. Extend isolation reset coverage to SQL SET statements and adjust close-error mocks to inject failures after construction.

Validation (Linux x64, Python 3.13, SQL Server 2022):

  • bash build.sh and subsequent cmake --build /work/mssql_python/pybind/build --config Release -j2: passed; rebuilt native extension used for testing.
  • python -m pytest -v --tb=short: 5,537 passed, 167 skipped, 42 deselected. Linux test copy used LF shell scripts to avoid Windows checkout CRLF failures.
  • python -m pre_commit run black-check --all-files --hook-stage pre-push: passed.
  • Actual TCP DROP reproduction: approximately 3 seconds with timeout=1 for one/three stale connections in both autocommit modes; replacement queries succeeded.
  • Windows/macOS validation not run. An initial combined packet-drop run exited with code 139; isolated and repeated runs subsequently passed, but the initial crash was not conclusively explained.

Fixes: #854

Apply a temporary native connection timeout during pooled reset and autocommit configuration, then restore it on success. Replace a failed candidate instead of probing all idle sockets sequentially.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 06:34
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ No regression detected

No consistent slowdowns detected across all 2 environments.

0 IMPROVEMENTS 0 SLOWDOWNS 2/2 ENVIRONMENTS

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Performance diagnostics

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

Unix / SQL Server 2022

Connection opening: no measured phase delta. Call changes: ddbc::Connection::setAutocommit (added, removed, or intermittent); ddbc::ConnectionHandle::setAutocommit (added, removed, or intermittent).

Unix / SQL Server 2025

Connection opening: no measured phase delta. Call changes: ddbc::Connection::setAutocommit (added, removed, or intermittent); ddbc::ConnectionHandle::setAutocommit (added, removed, or intermittent).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 11.061 ms 10.657 ms -2.0% no signal
SELECT queries 1.062 ms 1.056 ms -0.7% no signal
Row insertion 31.575 ms 31.589 ms -1.0% no signal
Executemany inserts 135.192 ms 134.038 ms -0.7% no signal
Fetch-all queries 117.667 ms 118.623 ms +1.2% no signal
Row-by-row fetching 12.620 ms 12.647 ms +0.3% no signal
Batched row fetching 110.814 ms 111.011 ms +0.1% no signal
Transaction commit and rollback 99.957 ms 100.050 ms -0.0% no signal
Arrow row fetching 90.141 ms 90.591 ms +0.7% no signal
100,000-row insertion 406.623 ms 399.052 ms -1.2% no signal
Row fetching in batches of 100 107.019 ms 108.405 ms +1.1% no signal
Row fetching in batches of 10,000 121.719 ms 119.712 ms -4.2% no signal
Repeated positional queries 30.115 ms 30.225 ms +0.7% no signal
Repeated named-parameter queries 32.430 ms 32.486 ms -0.5% no signal
Legacy 100,000-row insertion 310.455 ms 309.149 ms +0.1% no signal
Insertion with explicit input sizes 442.365 ms 432.465 ms +0.6% no signal
Joined aggregation queries 185.718 ms 186.433 ms -0.5% no signal
Large joined-result fetching 180.243 ms 179.826 ms -0.5% no signal
1.2-million-row fetching 3428.985 ms 3449.149 ms +0.6% no signal
Common table expression queries 5.562 ms 5.640 ms -0.2% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.275 ms 1.301 ms +2.9% no signal
10,000 scalar values / fetchval() (debug disabled) 101.293 ms 99.650 ms -0.5% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 98.940 ms 97.831 ms -0.7% no signal
SELECT queries 1.110 ms 1.173 ms +5.5% no signal
Row insertion 33.094 ms 32.405 ms -1.0% no signal
Executemany inserts 155.520 ms 153.339 ms -0.9% no signal
Fetch-all queries 123.349 ms 123.559 ms -1.0% no signal
Row-by-row fetching 14.518 ms 14.425 ms -1.5% no signal
Batched row fetching 119.764 ms 119.269 ms -1.0% no signal
Transaction commit and rollback 107.041 ms 108.619 ms +1.8% no signal
Arrow row fetching 90.986 ms 89.537 ms -1.8% no signal
100,000-row insertion 446.003 ms 441.518 ms -2.4% no signal
Row fetching in batches of 100 123.181 ms 123.725 ms +0.4% no signal
Row fetching in batches of 10,000 147.471 ms 133.514 ms -10.3% no signal
Repeated positional queries 31.854 ms 31.600 ms -0.7% no signal
Repeated named-parameter queries 34.060 ms 34.331 ms +0.8% no signal
Legacy 100,000-row insertion 348.675 ms 364.760 ms +4.6% no signal
Insertion with explicit input sizes 494.779 ms 494.563 ms -0.2% no signal
Joined aggregation queries 160.766 ms 161.454 ms +0.1% no signal
Large joined-result fetching 194.923 ms 199.226 ms +3.4% no signal
1.2-million-row fetching 3458.709 ms 3512.266 ms +1.1% no signal
Common table expression queries 5.155 ms 5.287 ms +2.5% no signal
256 KiB VARCHAR(MAX) / fetchall() 2.133 ms 2.188 ms +1.4% no signal
10,000 scalar values / fetchval() (debug disabled) 107.895 ms 108.226 ms +0.2% no signal
Build and measurement details

ADO build 184975

PR head: 7c582209d0383973f52059f5a674a15610695d12
Base: bd0aaf89c724a92c934b68bca41469655bfc733c
Measured merge: 01db4dfe6edf567239b8fed18fc6d66c04532404

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4305.5; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5015.4; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The reported native crash remains unexplained, and Windows/macOS timeout and cleanup behavior still needs validation.

1 open issue
What changed in this PR

Addresses #854 by limiting pooled-connection validation on unresponsive sockets without imposing an end-to-end connect() deadline.

Changes:

  • Applies a temporary native timeout during reset and autocommit configuration, restoring it after successful checkout.
  • Opens a replacement after failed validation instead of probing every stale idle connection.
  • Adds stalled-socket and isolation-reset regressions, adjusts exception mocks, and documents timeout behavior.
File Description
tests/​test_009_pooling.py Adds stalled-checkout, timeout-restoration, and SQL isolation-reset coverage.
tests/​test_006_exceptions.py Injects close failures after connection construction.
README.md Documents checkout timeout selection and limitations.
mssql_python/​pybind/​connection/​connection.h Extends reset to accept timeout and autocommit settings.
mssql_python/​pybind/​connection/​connection.cpp Applies and restores the temporary native timeout.
mssql_python/​pybind/​connection/​connection_pool.cpp Selects validation settings and replaces failed candidates directly.
mssql_python/​connection.py Passes autocommit into native checkout and documents timeout semantics.

Review effort: Balanced

Comment thread mssql_python/pybind/connection/connection.cpp
Read SQL_ATTR_CONNECTION_TIMEOUT through a private native accessor and check zero and nonzero values on the same reused SQL Server session. Mutation tests reject omitted restoration and unconditional zero.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 06:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The reported native crash remains unexplained, and Windows/macOS validation is outstanding for these connection-lifecycle changes.

1 open issue

Review effort: Balanced

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

70%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 9495 out of 11163
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/connection.py (100%)
  • mssql_python/pybind/connection/connection.cpp (67.4%): Missing lines 610-612,620-622,668-670,941-943,950-951
  • mssql_python/pybind/connection/connection_pool.cpp (70.4%): Missing lines 190-193,273-276

Summary

  • Total: 74 lines
  • Missing: 22 lines
  • Coverage: 70%

mssql_python/pybind/connection/connection.cpp

Lines 606-616

  606     }
  607     clearResultMetadata();
  608     // Login and statement timeouts do not bound connection-attribute calls.
  609     // Keep a finite connection timeout through reset and lease configuration;
! 610     // retain it on failure so discarding the candidate is bounded too (#854).
! 611     SQLULEN originalTimeout = 0;
! 612     SQLRETURN ret = SQLGetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_CONNECTION_TIMEOUT,
  613                                          &originalTimeout, 0, nullptr);
  614     if (!SQL_SUCCEEDED(ret)) {
  615         LOG("Failed to read pooled connection timeout (ret=%d). Marking as dead.", ret);
  616         return false;

Lines 616-626

  616         return false;
  617     }
  618     {
  619         py::gil_scoped_release release;
! 620         ret = SQLSetConnectAttr_ptr(
! 621             _dbcHandle->get(), SQL_ATTR_CONNECTION_TIMEOUT,
! 622             reinterpret_cast<SQLPOINTER>(static_cast<SQLULEN>(timeoutSeconds)), SQL_IS_UINTEGER);
  623     }
  624     if (!SQL_SUCCEEDED(ret)) {
  625         LOG("Failed to set pooled connection timeout (ret=%d). Marking as dead.", ret);
  626         return false;

Lines 664-674

  664     }
  665     setAutocommit(autocommit);
  666     {
  667         py::gil_scoped_release release;
! 668         ret = SQLSetConnectAttr_ptr(
! 669             _dbcHandle->get(), SQL_ATTR_CONNECTION_TIMEOUT,
! 670             reinterpret_cast<SQLPOINTER>(originalTimeout), SQL_IS_UINTEGER);
  671     }
  672     if (!SQL_SUCCEEDED(ret)) {
  673         LOG("Failed to restore pooled connection timeout (ret=%d). Marking as dead.", ret);
  674         return false;

Lines 937-947

  937 SQLULEN ConnectionHandle::getConnectionTimeout() const {
  938     if (!_conn) {
  939         ThrowStdException("Connection object is not initialized");
  940     }
! 941     SQLULEN timeout = 0;
! 942     const auto& handle = _conn->getDbcHandle();
! 943     SQLRETURN ret = SQLGetConnectAttr_ptr(handle->get(), SQL_ATTR_CONNECTION_TIMEOUT,
  944                                          &timeout, 0, nullptr);
  945     if (!SQL_SUCCEEDED(ret)) {
  946         ErrorInfo error = SQLCheckError_Wrap(SQL_HANDLE_DBC, handle, ret);
  947         ThrowStdException("Failed to read connection timeout: " + error.ddbcErrorMsg);

Lines 946-955

  946         ErrorInfo error = SQLCheckError_Wrap(SQL_HANDLE_DBC, handle, ret);
  947         ThrowStdException("Failed to read connection timeout: " + error.ddbcErrorMsg);
  948     }
  949     return timeout;
! 950 }
! 951 
  952 py::object ConnectionHandle::getInfo(SQLUSMALLINT infoType) const {
  953     PERF_TIMER("ConnectionHandle::getInfo");
  954     if (!_conn) {
  955         ThrowStdException("Connection object is not initialized");

mssql_python/pybind/connection/connection_pool.cpp

Lines 186-197

  186                     // expiry) before the threshold, so the safe-reuse path above
  187                     // is taken in practice. We favor never handing out a token
  188                     // that may expire mid-query over avoiding the churn.
  189                     candidate->setTokenExpiry(fresh_expiry);
! 190                     if (candidate->isAlive()) {
! 191                         validation_attempted = true;
! 192                         reuse_candidate = candidate->reset(validation_timeout, autocommit);
! 193                     }
  194                     if (!reuse_candidate) {
  195                         // The token is still valid but the socket is dead
  196                         // (isAlive()/reset() failed). We already minted the
  197                         // fresh attrs, so carry them forward and let Phase 3

Lines 269-280

  269                 ++_current_size;
  270                 needs_connect = true;
  271                 break;
  272             }
! 273             if (validation_attempted) {
! 274                 throw std::runtime_error(
! 275                     "ConnectionPool::acquire: pool size limit reached");
! 276             }
  277             // A concurrent acquire reserved the slot freed by token rotation.
  278             // Retry token comparison with another idle candidate.
  279         }
  280     }


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 62.6%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.connection.connection_pool.cpp: 81.3%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.fetch_temporal.hpp: 92.1%
mssql_python.cursor.py: 92.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI balanced review requested due to automatic review settings October 22, 2026 08:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The reported native crash remains unexplained, and Windows/macOS validation of the changed pooling lifecycle is outstanding.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PR for correctness, security, reliability, performance, test coverage, repository conventions, and applicable architecture and design specifications. No actionable issues were identified. The implementation is consistent with repository standards and the applicable approved design requirements.

Recommendation: Approve

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pooled connection checkout has no timeout: connect() hangs ~16 min on a dead socket

3 participants