[Draft] Async Query execution using RUST - POC#681
Draft
subrata-ms wants to merge 6 commits into
Draft
Conversation
Adds mssql_python/aio.py exposing AsyncConnection, AsyncCursor and the awaitable factory connect_async. Thin wrappers over the Rust mssql_py_core.PyCoreAsyncConnection / PyCoreAsyncCursor. - Every awaitable method is defined as 'async def foo(...): return await self._core.foo_async(...)' so callers can use asyncio.create_task / gather / wait_for on them. Rust future_into_py returns an awaitable (not a coroutine); create_task() would otherwise TypeError. - AsyncCursor.execute takes (operation, timeout_sec=30) and returns self. - AsyncCursor.fetchone returns tuple or None. - AsyncCursor.cancel is sync and safe to call from another task. - AsyncConnection.connect classmethod + module-level connect_async factory keep the async idiom even though connect is currently sync under the hood. - Both classes support async context managers (__aenter__ / __aexit__) and expose .closed. Idempotent close(). - Use-after-close raises InterfaceError with a clean driver/DDBC message. Wired up in mssql_python/__init__.py so users get 'mssql_python.connect_async', 'mssql_python.AsyncConnection', and 'mssql_python.AsyncCursor' via the top-level package. End-to-end wrapper smoke against local SQL Server 2022 container: - basic: SELECT 42, multi-col decode, exhaustion, re-execute - non-blocking: 60 ticks during 400 ms WAITFOR (elapsed 0.412s) - concurrent: 2x500ms queries via asyncio.gather in 0.542s - cancel: WAITFOR '00:00:10' cancelled via asyncio.create_task, exception in 3 ms - after-close: execute/fetchone raise InterfaceError Regression: tests/test_019_bulkcopy.py still 10/10 passing.
- pytest.ini: register the 'asyncio' marker and set asyncio_mode=strict so
each async test opts in explicitly (via module-level pytestmark). This
leaves the 678-test sync suite unchanged.
- tests/conftest.py: add async fixtures (async_conn_str session-scoped,
async_connection + async_cursor function-scoped) inline so pytest
auto-loads them; skip the async suite if DB_CONNECTION_STRING is unset.
Function scope keeps cancellation / close tests independent.
- tests/test_030_async_poc.py: 13 tests across 4 classes exercising the
full POC surface end-to-end:
* TestAsyncBasics (6): literal, multi-col, exhaustion, multi-row,
re-execute, and async context-manager idiom via connect_async.
* TestAsyncErrors (4): bad SQL, execute/fetchone after cursor close,
cursor() after connection close.
* TestAsyncNonBlocking (2): ticker keeps ticking during a 400 ms
WAITFOR (proves loop non-blocking); two connections × 500 ms in
< 0.9 s via asyncio.gather (proves multi-thread runtime).
* TestAsyncCancel (1): asyncio.create_task + cur.cancel() aborts a
10 s WAITFOR in < 5 s.
Results:
- tests/test_030_async_poc.py: 13 passed in 1.54 s.
- Regression tests/test_019_bulkcopy.py + tests/test_003_connection.py +
tests/test_004_cursor.py: 678 passed / 5 skipped / 4 warnings in 36 s.
- Combined pytest tests/test_019_bulkcopy.py tests/test_030_async_poc.py:
23 passed in 1.99 s (async + sync cohabit cleanly).
Note: the earlier attempt at 'pytest_plugins = tests.conftest_async' failed
because tests/ has no __init__.py. Folded fixtures into tests/conftest.py
directly — simplest and idiomatic.
Keeps ASYNC_POC_SPEC.md / ASYNC_POC_RUNBOOK.md as personal working notes without committing them upstream. History for the docs commit (previously 9fe5fe6) has been rebase-dropped from this branch.
5b1ebb4 to
f9eab1e
Compare
Mirrors the sync pair (cursor.py / connection.py) — one class per file, keeping module names discoverable by their responsibility. - mssql_python/cursor_async.py AsyncCursor - mssql_python/connection_async.py AsyncConnection + connect_async factory - mssql_python/aio.py deleted Public surface unchanged: mssql_python.AsyncConnection, AsyncCursor and connect_async are still re-exported from the package __init__ and remain the recommended entry points for user code. Verified: 23/23 async + bulkcopy tests pass after the split.
Two tests in a new TestAsyncScale class exercise the pyo3-async-runtimes + Tokio pipeline under real concurrent load: - test_hundred_connections_correctness: 100 tasks, each opening a fresh AsyncConnection, running SELECT n, fetchone, and closing. Verifies every value round-trips and the pipeline handles 100 concurrent sessions from a single Python process. - test_hundred_connections_true_parallelism: 100 tasks each running a 200 ms server-side WAITFOR. Asserts wall time is well below the 20 s serial baseline, proving the multi-thread Tokio runtime + non-blocking future_into_py futures actually run in parallel. Both marked @pytest.mark.slow (currently included by default; can be excluded via pytest -m 'not slow' if a runner is resource-constrained). Measurements against local SQL Server 2022 container: - 100 connect+select+close cycles: 0.963 s (~10 ms/conn amortised) - 100 x 200 ms WAITFOR: 1.107 s (~18x over serial baseline) Combined pytest tests/test_030_async_poc.py tests/test_019_bulkcopy.py: 25 passed in 3.51 s.
…query
Replaces the trivial SELECT / WAITFOR stubs with a T-SQL batch that is
both non-trivial and predictably verifiable:
WAITFOR DELAY ... -- keeps it long-running
WITH nums AS ( recursive CTE producing 50 rows )
SELECT
task_id INT, -- interpolated identity
n INT, -- row order 1..50
square INT (n*n), -- integer arithmetic
label NVARCHAR, -- per-row string with task_id + n
root FLOAT (SQRT(n)) -- floating-point decode
FROM nums
OPTION (MAXRECURSION 50)
Every value is server-computed, so any driver-side corruption (wrong
column order, truncated string, wrong float bits, wrong integer width)
surfaces as an assertion failure. Row-by-row validation across 5 000
rows per test proves the streaming fetchone_async decode is correct at
scale, not just for a single sentinel value.
Helpers extracted for readability:
- _complex_long_running_query(task_id, wait_ms) — batch builder.
- _verify_task_rows(task_id, rows) — deterministic per-row assertions.
Tests updated:
- test_hundred_connections_complex_query_correctness (100 ms wait)
- test_hundred_connections_long_running_parallelism (200 ms wait)
Measurements against local SQL Server 2022 container:
- Correctness (5 000 rows verified, 100 ms wait/task): 1.782 s @ 2 806 r/s
- Parallelism (5 000 rows verified, 200 ms wait/task): 1.791 s @ 2 791 r/s
(serial baseline ~20 s just for the WAITFORs)
Combined pytest tests/test_030_async_poc.py tests/test_019_bulkcopy.py:
25 passed in 5.17 s.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work Item / Issue Reference
Summary