microsoft / microsoft/mssql-rs
Command timeout is not enforced as a wall-clock deadline while TdsClient::advance() drains continuously available rows
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 53
- Forks
- 14
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 137
Description
### Describe the bug
A positive timeout supplied through ExecuteOptions::timeout_secs() is documented as bounding the command in tds_client.rs:6291-6319. However, TdsClient::advance() can substantially exceed that timeout while draining unread rows from the current result set.
advance() invokes drain_rows() before moving to the next result in tds_client.rs:4316-4325. The drain loop repeatedly calls next_row_into() in tds_client.rs:3715-3738.
**Timeout accounting currently:**
Stores a remaining duration when the command begins.
Measures and subtracts elapsed time around individual receive operations in tds_client.rs:1030-1039.
Applies a separate Tokio timeout to each receive_row_into() operation in token_stream.rs:861-892.
When rows are continuously available or buffered, each receive operation can complete immediately. Consequently, the per-receive timeout does not provide a strict wall-clock deadline for the entire drain operation. Processing and scheduling time between receive calls is also outside each measured interval.
### Steps to reproduce
1. Connect to SQL Server using mssql-tds with optional encryption.
2. Configure a one-second command timeout using ExecuteOptions::new().timeout_secs(1).
3. Execute a batch whose first result contains a large number of continuously available rows:
SELECT TOP (10000000)
REPLICATE(CAST('x' AS varchar(100)), 100)
FROM sys.all_objects AS first_rows
CROSS JOIN sys.all_objects AS second_rows;
SELECT 2;
4. Allow execution to position on the first result set.
5. Start a wall-clock timer.
6. Call TdsClient::advance() without reading the first result set. This causes advance() to drain all unread rows before positioning on SELECT 2.
7. Record the result and elapsed wall-clock time.
**Equivalent reproduction through the async py-core wrapper:**
1. Set connection.timeout = 1.
2. Execute the batch.
3. Measure await cursor.nextset().
The latest reproduction used SQL Server 2025 and returned:
- Outcome: success
- nextset() result: True
- Elapsed time: 12.495 seconds
### Expected behavior
The positive one-second per-request timeout should bound the complete command operation.
While advance() is draining the unread result, it should detect that the command deadline has expired, cancel the active request, and return a timeout error at approximately one second, subject only to a small bounded cancellation-cleanup allowance.
It should not successfully advance to the next result after spending approximately 12 seconds inside a command configured with a one-second timeout.
### Actual behavior
TdsClient::advance() drains all ten million rows and successfully advances to the second result.
A one-second timeout completed successfully after 12.495 seconds, with no native timeout error.
Caller-level asyncio.wait_for() currently provides the reliable wall-clock bound, but cancellation through that mechanism makes the py-core session unusable. It is a workaround rather than native command-timeout enforcement.
### Version
mssql-tds crate: 0.1.0, as declared in Cargo.toml:1-3
### Affected crate
mssql-tds
### Environment
WSL Ubuntu
### Additional context
_No response_
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Trace TdsClient::advance() and drain_rows() in tds_client.rs:4316-4325 and 3715-3738, then compare their timeout handling with command setup at tds_client.rs:6291-6319 and receive logic in tds_client.rs:1030-1039 and token_stream.rs:861-892. Reproduce with the supplied large-row SQL batch and verify that a one-second timeout stops draining and returns a timeout error rather than advancing after the deadline.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100