microsoft / microsoft/mssql-rs

Command timeout is not enforced as a wall-clock deadline while TdsClient::advance() drains continuously available rows

Open
#468 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.