Skip to content

ASYNC Probe#676

Draft
subrata-ms wants to merge 1 commit into
mainfrom
AsyncQuery_POC
Draft

ASYNC Probe#676
subrata-ms wants to merge 1 commit into
mainfrom
AsyncQuery_POC

Conversation

@subrata-ms

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#<WORK_ITEM_ID>

GitHub Issue: #<ISSUE_NUMBER>


Summary

"""Probe the ODBC driver's async capability and verify polling works end-to-end.

Run:
export DB_CONNECTION_STRING="Server=localhost,1433;Database=master;UID=sa;PWD=...;TrustServerCertificate=Yes;Encrypt=Yes"
"Ratios BELOW ~0.5x would indicate a driver-side global lock — "
"that is what we")
_row(" ",
"actually gate on. Any value between 0.5x and ~1.5x on localhost "
@github-actions

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

2%


🎯 Overall Coverage

78%


📈 Total Lines Covered: 6747 out of 8625
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/pybind/connection/connection.cpp (2.8%): Missing lines 656,659-667,669-670,673-692,698-709,712-723,730-743,755-757,759-779,782-784,787-806,810,813-840,842,846-852,865-867,869-892,895-900,904-924,926,932-943,946-986,988,990-996,998-999,1001-1006,1010-1011

Summary

  • Total: 288 lines
  • Missing: 280 lines
  • Coverage: 2%

mssql_python/pybind/connection/connection.cpp

Lines 652-696

  652 // enabling SQL_ATTR_ASYNC_ENABLE on a fresh statement handle.
  653 //
  654 // The existing generic Connection::getInfo returns raw bytes; for these
  655 // numeric infotypes we want the value as an int, decoded on the C++ side.
! 656 // -----------------------------------------------------------------------------
  657 
  658 // SQL_ASYNC_STMT_FUNCTIONS is defined by the Microsoft ODBC headers but is
! 659 // missing from some unixODBC releases. The numeric ID is stable across the
! 660 // spec (10005), so provide a local fallback.
! 661 #ifndef SQL_ASYNC_STMT_FUNCTIONS
! 662 #define SQL_ASYNC_STMT_FUNCTIONS 10005
! 663 #endif
! 664 
! 665 py::dict Connection::getAsyncCapability() const {
! 666     if (!_dbcHandle) {
! 667         ThrowStdException("Connection handle not allocated");
  668     }
! 669     if (!SQLGetInfo_ptr || !SQLAllocHandle_ptr || !SQLSetStmtAttr_ptr ||
! 670         !SQLFreeHandle_ptr) {
  671         LOG("getAsyncCapability: driver function pointers not initialized, loading driver");
  672         DriverLoader::getInstance().loadDriver();
! 673     }
! 674 
! 675     SQLHDBC hDbc = _dbcHandle->get();
! 676     py::dict result;
! 677 
! 678     // --- SQL_ASYNC_MODE (SQLUSMALLINT) -----------------------------------
! 679     {
! 680         SQLUSMALLINT asyncMode = 0;
! 681         SQLSMALLINT outLen = 0;
! 682         SQLRETURN ret =
! 683             SQLGetInfo_ptr(hDbc, SQL_ASYNC_MODE, &asyncMode, sizeof(asyncMode), &outLen);
! 684         if (SQL_SUCCEEDED(ret)) {
! 685             result["async_mode"] = static_cast<unsigned int>(asyncMode);
! 686             const char* name = "SQL_AM_NONE";
! 687             if (asyncMode == SQL_AM_CONNECTION) {
! 688                 name = "SQL_AM_CONNECTION";
! 689             } else if (asyncMode == SQL_AM_STATEMENT) {
! 690                 name = "SQL_AM_STATEMENT";
! 691             }
! 692             result["async_mode_name"] = std::string(name);
  693         } else {
  694             LOG("getAsyncCapability: SQLGetInfo(SQL_ASYNC_MODE) failed - "
  695                 "SQLRETURN=%d",
  696                 ret);

Lines 694-727

  694             LOG("getAsyncCapability: SQLGetInfo(SQL_ASYNC_MODE) failed - "
  695                 "SQLRETURN=%d",
  696                 ret);
  697             result["async_mode"] = py::none();
! 698             result["async_mode_name"] = py::none();
! 699         }
! 700     }
! 701 
! 702     // --- SQL_ASYNC_STMT_FUNCTIONS (SQLUINTEGER bitmask) ------------------
! 703     // We return the raw bitmask value. Interpreting the bitmask across
! 704     // drivers is unreliable, so the functional check below is the real
! 705     // gate. This value is exposed for informational logging only.
! 706     {
! 707         SQLUINTEGER stmtFns = 0;
! 708         SQLSMALLINT outLen = 0;
! 709         SQLRETURN ret = SQLGetInfo_ptr(hDbc, SQL_ASYNC_STMT_FUNCTIONS, &stmtFns,
  710                                        sizeof(stmtFns), &outLen);
  711         if (SQL_SUCCEEDED(ret)) {
! 712             result["async_stmt_functions_bitmask"] = static_cast<unsigned int>(stmtFns);
! 713         } else {
! 714             LOG("getAsyncCapability: SQLGetInfo(SQL_ASYNC_STMT_FUNCTIONS) not "
! 715                 "reported by driver - SQLRETURN=%d",
! 716                 ret);
! 717             result["async_stmt_functions_bitmask"] = py::none();
! 718         }
! 719     }
! 720 
! 721     // --- SQL_ASYNC_DBC_FUNCTIONS (SQLUINTEGER bitmask, ODBC 3.8+) --------
! 722     {
! 723         SQLUINTEGER dbcFns = 0;
  724         SQLSMALLINT outLen = 0;
  725         SQLRETURN ret =
  726             SQLGetInfo_ptr(hDbc, SQL_ASYNC_DBC_FUNCTIONS, &dbcFns, sizeof(dbcFns), &outLen);
  727         if (SQL_SUCCEEDED(ret)) {

Lines 726-747

  726             SQLGetInfo_ptr(hDbc, SQL_ASYNC_DBC_FUNCTIONS, &dbcFns, sizeof(dbcFns), &outLen);
  727         if (SQL_SUCCEEDED(ret)) {
  728             result["async_dbc_functions_bitmask"] = static_cast<unsigned int>(dbcFns);
  729         } else {
! 730             LOG("getAsyncCapability: SQLGetInfo(SQL_ASYNC_DBC_FUNCTIONS) not "
! 731                 "supported by driver - SQLRETURN=%d",
! 732                 ret);
! 733             result["async_dbc_functions_bitmask"] = py::none();
! 734         }
! 735     }
! 736 
! 737     // --- SQL_ASYNC_NOTIFICATION (SQLUINTEGER, ODBC 3.8+) -----------------
! 738     // Returns SQL_ASYNC_NOTIFICATION_CAPABLE (1) or SQL_ASYNC_NOTIFICATION_NOT_CAPABLE (0).
! 739     // Tells us whether the driver supports the event-driven / callback
! 740     // completion path (SQL_ATTR_ASYNC_STMT_EVENT / SQL_ATTR_ASYNC_STMT_PCALLBACK)
! 741     // as an alternative to polling.
! 742     {
! 743         SQLUINTEGER notify = 0;
  744         SQLSMALLINT outLen = 0;
  745         SQLRETURN ret =
  746             SQLGetInfo_ptr(hDbc, SQL_ASYNC_NOTIFICATION, &notify, sizeof(notify), &outLen);
  747         if (SQL_SUCCEEDED(ret)) {

Lines 751-856

  751             LOG("getAsyncCapability: SQLGetInfo(SQL_ASYNC_NOTIFICATION) not "
  752                 "reported by driver - SQLRETURN=%d",
  753                 ret);
  754             result["async_notification"] = py::none();
! 755             result["async_notification_capable"] = py::none();
! 756         }
! 757     }
  758 
! 759     // --- Functional smoke test: run WAITFOR + SELECT under polling --------
! 760     // This is the real gate. We allocate an HSTMT, enable statement-level
! 761     // async, and drive a short server-side delay through the polling loop:
! 762     //   1. SQLExecDirect returns SQL_STILL_EXECUTING while the server sleeps
! 763     //   2. Re-invoke SQLExecDirect until it returns SQL_SUCCESS
! 764     //   3. SQLFetch may also return SQL_STILL_EXECUTING; poll it too
! 765     // A pass here proves polling works end-to-end on this OS + driver.
! 766     // mssql-python loads msodbcsql directly via dlopen / LoadLibraryW (see
! 767     // LoadDriverLibrary in ddbc_bindings.cpp), so this test is independent
! 768     // of any ODBC Driver Manager (unixODBC / iODBC / Windows DM).
! 769     {
! 770         py::dict smoke;
! 771         smoke["ran"] = false;
! 772 
! 773         if (!SQLExecDirect_ptr || !SQLFetch_ptr || !SQLFreeStmt_ptr) {
! 774             smoke["error"] = std::string(
! 775                 "polling smoke test skipped: required driver function pointers not initialized");
! 776             result["polling_smoke_test"] = smoke;
! 777         } else {
! 778             SQLHANDLE hStmt = nullptr;
! 779             SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_STMT, hDbc, &hStmt);
  780             if (!SQL_SUCCEEDED(ret) || hStmt == nullptr) {
  781                 smoke["error"] = std::string("SQLAllocHandle(SQL_HANDLE_STMT) failed");
! 782                 smoke["sqlreturn"] = static_cast<int>(ret);
! 783                 result["polling_smoke_test"] = smoke;
! 784             } else {
  785                 ret = SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ASYNC_ENABLE,
  786                                          (SQLPOINTER)(uintptr_t)SQL_ASYNC_ENABLE_ON, 0);
! 787                 smoke["set_async_enable_sqlreturn"] = static_cast<int>(ret);
! 788                 if (!SQL_SUCCEEDED(ret)) {
! 789                     smoke["error"] =
! 790                         std::string("SQLSetStmtAttr(SQL_ATTR_ASYNC_ENABLE) failed");
! 791                     SQLFreeHandle_ptr(SQL_HANDLE_STMT, hStmt);
! 792                     result["polling_smoke_test"] = smoke;
! 793                 } else {
! 794                     // 1-second server-side sleep guarantees the initial call
! 795                     // returns SQL_STILL_EXECUTING at least once.
! 796                     std::u16string queryStr =
! 797                         u"WAITFOR DELAY '00:00:01'; SELECT 1 AS async_probe";
! 798                     SQLWCHAR* queryPtr = reinterpretU16stringAsSqlWChar(queryStr);
! 799 
! 800                     // --- SQLExecDirect polling loop -----------------------
! 801                     using Clock = std::chrono::steady_clock;
! 802                     auto execStart = Clock::now();
! 803                     unsigned int execPollCount = 0;
! 804                     SQLRETURN execRet;
! 805                     {
! 806                         py::gil_scoped_release release;
  807                         execRet = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS);
  808                         while (execRet == SQL_STILL_EXECUTING) {
  809                             ++execPollCount;
! 810                             std::this_thread::sleep_for(std::chrono::milliseconds(10));
  811                             execRet = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS);
  812                         }
! 813                     }
! 814                     auto execMs = std::chrono::duration_cast<std::chrono::milliseconds>(
! 815                                       Clock::now() - execStart)
! 816                                       .count();
! 817                     smoke["execute_sqlreturn"] = static_cast<int>(execRet);
! 818                     smoke["execute_ok"] = SQL_SUCCEEDED(execRet);
! 819                     smoke["execute_poll_count"] = execPollCount;
! 820                     smoke["execute_elapsed_ms"] = static_cast<long long>(execMs);
! 821                     // observed_still_executing is the critical evidence: if
! 822                     // this is false, either the driver blocked internally or
! 823                     // the query completed too quickly (unlikely with WAITFOR).
! 824                     smoke["execute_observed_still_executing"] = (execPollCount > 0);
! 825 
! 826                     // --- SQLFetch polling loop ----------------------------
! 827                     if (SQL_SUCCEEDED(execRet)) {
! 828                         auto fetchStart = Clock::now();
! 829                         unsigned int fetchPollCount = 0;
! 830                         SQLRETURN fetchRet;
! 831                         {
! 832                             py::gil_scoped_release release;
! 833                             fetchRet = SQLFetch_ptr(hStmt);
! 834                             while (fetchRet == SQL_STILL_EXECUTING) {
! 835                                 ++fetchPollCount;
! 836                                 std::this_thread::sleep_for(std::chrono::milliseconds(5));
! 837                                 fetchRet = SQLFetch_ptr(hStmt);
! 838                             }
! 839                         }
! 840                         auto fetchMs = std::chrono::duration_cast<std::chrono::milliseconds>(
  841                                            Clock::now() - fetchStart)
! 842                                            .count();
  843                         smoke["fetch_sqlreturn"] = static_cast<int>(fetchRet);
  844                         smoke["fetch_ok"] = SQL_SUCCEEDED(fetchRet);
  845                         smoke["fetch_poll_count"] = fetchPollCount;
! 846                         smoke["fetch_elapsed_ms"] = static_cast<long long>(fetchMs);
! 847                         smoke["fetch_observed_still_executing"] = (fetchPollCount > 0);
! 848                     } else {
! 849                         smoke["fetch_sqlreturn"] = py::none();
! 850                         smoke["fetch_ok"] = false;
! 851                         smoke["fetch_poll_count"] = py::none();
! 852                         smoke["fetch_elapsed_ms"] = py::none();
  853                         smoke["fetch_observed_still_executing"] = py::none();
  854                     }
  855 
  856                     smoke["ran"] = true;

Lines 861-930

  861                     SQLFreeHandle_ptr(SQL_HANDLE_STMT, hStmt);
  862                     result["polling_smoke_test"] = smoke;
  863                 }
  864             }
! 865         }
! 866     }
! 867 
  868     // --- Fetch-stream smoke test ------------------------------------------
! 869     // The polling smoke test above uses a query that returns a single tiny
! 870     // row — SQLFetch completes instantly from the TCP receive buffer with
! 871     // zero polling, so it does NOT exercise async fetch. This second test
! 872     // deliberately streams a multi-megabyte result set so that SQLFetch has
! 873     // to wait on TDS packet arrivals from the network. That is where
! 874     // SQL_STILL_EXECUTING on SQLFetch actually shows up in practice.
! 875     //
! 876     // Query is deliberately server-heavy: TOP 50000 rows from a cross-join
! 877     // of sys.all_objects, with three moderately wide columns per row. On a
! 878     // stock master DB this yields several MB and spans ~1000 TDS packets.
! 879     {
! 880         py::dict fetchStream;
! 881         fetchStream["ran"] = false;
! 882 
! 883         if (!SQLExecDirect_ptr || !SQLFetch_ptr || !SQLFreeStmt_ptr) {
! 884             fetchStream["error"] = std::string(
! 885                 "fetch-stream test skipped: required driver function pointers "
! 886                 "not initialized");
! 887             result["polling_fetch_stream_test"] = fetchStream;
! 888         } else {
! 889             SQLHANDLE hStmt = nullptr;
! 890             SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_STMT, hDbc, &hStmt);
! 891             if (!SQL_SUCCEEDED(ret) || hStmt == nullptr) {
! 892                 fetchStream["error"] =
  893                     std::string("SQLAllocHandle(SQL_HANDLE_STMT) failed");
  894                 fetchStream["sqlreturn"] = static_cast<int>(ret);
! 895                 result["polling_fetch_stream_test"] = fetchStream;
! 896             } else {
! 897                 ret = SQLSetStmtAttr_ptr(
! 898                     hStmt, SQL_ATTR_ASYNC_ENABLE,
! 899                     (SQLPOINTER)(uintptr_t)SQL_ASYNC_ENABLE_ON, 0);
! 900                 fetchStream["set_async_enable_sqlreturn"] = static_cast<int>(ret);
  901                 if (!SQL_SUCCEEDED(ret)) {
  902                     fetchStream["error"] =
  903                         std::string("SQLSetStmtAttr(SQL_ATTR_ASYNC_ENABLE) failed");
! 904                     SQLFreeHandle_ptr(SQL_HANDLE_STMT, hStmt);
! 905                     result["polling_fetch_stream_test"] = fetchStream;
! 906                 } else {
! 907                     // ~50000 rows × 3 columns from a cross-join. Uses only
! 908                     // system catalog views so it runs on any SQL Server DB.
! 909                     std::u16string queryStr = u"SELECT TOP 50000 "
! 910                         u"CAST(a.object_id AS BIGINT) AS id, "
! 911                         u"CAST(a.name AS NVARCHAR(128)) AS n, "
! 912                         u"CAST(a.create_date AS DATETIME2) AS cd "
! 913                         u"FROM sys.all_objects a CROSS JOIN sys.all_objects b";
! 914                     SQLWCHAR* queryPtr = reinterpretU16stringAsSqlWChar(queryStr);
! 915 
! 916                     // Poll the execute first — this may or may not observe
! 917                     // STILL_EXECUTING depending on server plan-cache state.
! 918                     using Clock = std::chrono::steady_clock;
! 919                     auto execStart = Clock::now();
! 920                     unsigned int execPollCount = 0;
! 921                     SQLRETURN execRet;
! 922                     {
! 923                         py::gil_scoped_release release;
! 924                         execRet = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS);
  925                         while (execRet == SQL_STILL_EXECUTING) {
! 926                             ++execPollCount;
  927                             std::this_thread::sleep_for(std::chrono::milliseconds(10));
  928                             execRet = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS);
  929                         }
  930                     }

Lines 928-1015

   928                             execRet = SQLExecDirect_ptr(hStmt, queryPtr, SQL_NTS);
   929                         }
   930                     }
   931                     auto execMs = std::chrono::duration_cast<std::chrono::milliseconds>(
!  932                                       Clock::now() - execStart)
!  933                                       .count();
!  934                     fetchStream["execute_sqlreturn"] = static_cast<int>(execRet);
!  935                     fetchStream["execute_ok"] = SQL_SUCCEEDED(execRet);
!  936                     fetchStream["execute_poll_count"] = execPollCount;
!  937                     fetchStream["execute_elapsed_ms"] =
!  938                         static_cast<long long>(execMs);
!  939 
!  940                     if (SQL_SUCCEEDED(execRet)) {
!  941                         // Stream all rows. For each SQLFetch call, count how
!  942                         // many STILL_EXECUTING returns we saw before it
!  943                         // completed. Rows are not bound/materialized — we're
   944                         // only measuring whether the fetch phase EVER blocks
   945                         // on the network and how often.
!  946                         auto fetchStart = Clock::now();
!  947                         unsigned int rowsRead = 0;
!  948                         unsigned int totalPollCount = 0;
!  949                         unsigned int rowsThatPolled = 0;
!  950                         SQLRETURN fetchRet = SQL_SUCCESS;
!  951                         {
!  952                             py::gil_scoped_release release;
!  953                             while (true) {
!  954                                 unsigned int rowPolls = 0;
!  955                                 fetchRet = SQLFetch_ptr(hStmt);
!  956                                 while (fetchRet == SQL_STILL_EXECUTING) {
!  957                                     ++rowPolls;
!  958                                     // 500us — tight enough to catch short
!  959                                     // network waits without spinning.
!  960                                     std::this_thread::sleep_for(
!  961                                         std::chrono::microseconds(500));
!  962                                     fetchRet = SQLFetch_ptr(hStmt);
!  963                                 }
!  964                                 if (fetchRet == SQL_NO_DATA) {
!  965                                     break;
!  966                                 }
!  967                                 if (!SQL_SUCCEEDED(fetchRet)) {
!  968                                     break;
!  969                                 }
!  970                                 ++rowsRead;
!  971                                 totalPollCount += rowPolls;
!  972                                 if (rowPolls > 0) {
!  973                                     ++rowsThatPolled;
!  974                                 }
!  975                             }
!  976                         }
!  977                         auto fetchMs = std::chrono::duration_cast<std::chrono::milliseconds>(
!  978                                            Clock::now() - fetchStart)
!  979                                            .count();
!  980                         fetchStream["fetch_final_sqlreturn"] =
!  981                             static_cast<int>(fetchRet);
!  982                         fetchStream["fetch_ok"] =
!  983                             (fetchRet == SQL_NO_DATA) ||
!  984                             SQL_SUCCEEDED(fetchRet);
!  985                         fetchStream["fetch_rows_read"] = rowsRead;
!  986                         fetchStream["fetch_total_poll_count"] = totalPollCount;
   987                         fetchStream["fetch_rows_that_polled"] = rowsThatPolled;
!  988                         fetchStream["fetch_elapsed_ms"] =
   989                             static_cast<long long>(fetchMs);
!  990                         fetchStream["fetch_observed_still_executing"] =
!  991                             (totalPollCount > 0);
!  992                     } else {
!  993                         fetchStream["fetch_final_sqlreturn"] = py::none();
!  994                         fetchStream["fetch_ok"] = false;
!  995                         fetchStream["fetch_rows_read"] = py::none();
!  996                         fetchStream["fetch_total_poll_count"] = py::none();
   997                         fetchStream["fetch_rows_that_polled"] = py::none();
!  998                         fetchStream["fetch_elapsed_ms"] = py::none();
!  999                         fetchStream["fetch_observed_still_executing"] = py::none();
  1000                     }
! 1001 
! 1002                     fetchStream["ran"] = true;
! 1003 
! 1004                     SQLFreeStmt_ptr(hStmt, SQL_CLOSE);
! 1005                     SQLFreeHandle_ptr(SQL_HANDLE_STMT, hStmt);
! 1006                     result["polling_fetch_stream_test"] = fetchStream;
  1007                 }
  1008             }
  1009         }
! 1010     }
! 1011 
  1012     return result;
  1013 }
  1014 
  1015 py::dict ConnectionHandle::getAsyncCapability() const {


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.connection.connection.cpp: 46.5%
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 76.2%
mssql_python.__init__.py: 77.3%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.connection.py: 83.6%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@subrata-ms subrata-ms changed the title ASYNC POC code ASYNC Probe Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants