Add first-class coroutine PyCoreAsyncCursor to mssql-py-core - #208
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a coroutine-based Python cursor that performs TDS operations without blocking the asyncio event loop.
Changes:
- Adds and registers
PyCoreAsyncCursorandConnection.async_cursor(). - Adds asynchronous client checkout and the required runtime dependency.
- Adds mock and live-server coroutine tests and updates cursor terminology.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
mssql-py-core/Cargo.toml |
Adds the PyO3 async runtime dependency. |
mssql-py-core/src/async_cursor.rs |
Implements the coroutine cursor. |
mssql-py-core/src/connection.rs |
Exposes async_cursor(). |
mssql-py-core/src/lib.rs |
Registers the new Python class. |
mssql-py-core/src/pyclient.rs |
Adds asynchronous client checkout. |
mssql-py-core/tests/rs-only-tests/test_async_cursor_mock.py |
Tests coroutine cursor behavior. |
mssql-py-core/tests/rs-only-tests/test_sync_async_cursor_mock.py |
Corrects default cursor terminology. |
mssql-py-core/tests/test_async_cursor_integration.py |
Verifies live-server non-blocking behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| &self, | ||
| py: Python<'py>, | ||
| query: String, | ||
| params: Option<Vec<Py<PyAny>>>, |
| let taken = { | ||
| let mut guard = cell.lock().map_err(|_| poisoned())?; | ||
| std::mem::replace(&mut *guard, PyClient::Transitioning) |
Add PyCoreAsyncCursor, a genuine asyncio coroutine cursor built on pyo3-async-runtimes future_into_py. execute/fetchone/fetchmany/fetchall/ close return awaitables that drive the TDS I/O on the connection's tokio runtime with no block_on on the coroutine path. The !Send client is checked out of the shared cell and the guard dropped before .await, so the Python event loop stays free during fetches. Strictly additive: the default PyCoreCursor and opt-in PyCoreSyncCursor are byte-identical to their prior form. Also scrubs stale 'async cursor' doc labels now that a real coroutine cursor exists. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e2c378f8-3ba1-4b48-9ebe-5a4ea2bd2761
dbee5b3 to
0fbfb5e
Compare
|
Closing, consistent with the rest of the sans-I/O stack (#189–#203, closed 2026-08-13). This PR is an L5–L7 consumer of the reactor-free sync core, so it has no base once that core is not landing. The stack was closed because the row-decode performance work that motivated it was re-scoped around #247, where benchmarked spikes showed the dominant win came from a far smaller change than inverting the core to a sync That argument has since been settled by measurement rather than projection. PR #264 has now merged ( Two further results from that work are worth recording here, because both cut against restructuring this path speculatively:
This is cleanup, not a rejection of the analysis. The branch is deliberately not deleted, so the work remains recoverable if the direction is revisited. |
Summary
Adds
PyCoreAsyncCursor, a genuineasynciocoroutine cursor formssql-py-core, built onpyo3-async-runtimesfuture_into_py. This is a correctness/architecture change (a real non-blocking coroutine cursor), not a performance win.execute/fetchone/fetchmany/fetchall/closeeach return a Python awaitable. The TDS I/O is driven on the connection's own tokio runtime and the coroutine.awaits the resulting join handle — there is noblock_onon the coroutine path. The!Sendclient is checked out of the shared cell and the guard is dropped before.await, so the Python event loop stays free while a fetch is in flight.Strictly additive
PyCoreCursor(conn.cursor()) and the opt-in syncPyCoreSyncCursor(conn.sync_cursor()) are byte-identical to their prior form —cursor.rsandsync_cursor.rsare unchanged.conn.async_cursor()→PyCoreAsyncCursor.with_async_clientcheckout helper inpyclient.rs; the shared client cell staysstd::sync::Mutex(no retype needed).Changes (py-core only)
src/async_cursor.rs(new) —PyCoreAsyncCursorcoroutine cursor.src/pyclient.rs—with_async_clientasync checkout helper + honest module doc.src/connection.rs—Connection.async_cursor().src/lib.rs— registerPyCoreAsyncCursor.Cargo.toml— addpyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }.WAITFOR-based non-blocking proof.Non-blocking proof
block_onon theasync_cursor.rscoroutine path (I/O is spawned on the connection runtime; the coroutine awaits the join handle).test_async_cursor_yields_to_event_loop— a concurrent ticker advances while a fetch is awaited (mock answers in µs, so this asserts the weaker "yields at least once").test_async_cursor_integration.py— a concurrent 10ms ticker racks up ticks during a server-sideWAITFOR DELAY '00:00:02'inside the coroutine'sawait.Notes
block_onpath — this is pure coroutine-plumbing overhead on a zero-latency backend, not representative of real network I/O where the overhead is negligible and concurrency dominates.mssql-py-coreCargo.lockis gitignored; the new dep resolves under the--frozenpy-core clippy/test leg locally.Part of the sans-I/O native stack - see cover PR #189 for the stack-level summary and tracked debt.