Skip to content

Mock TDS server hangs autocommit-off clients: TransactionManager (0x0E) requests are unhandled #86

Description

@saurabh500

Summary

The mock TDS server (mssql-mock-tds / mssql-mock-tds-py) hangs any client that connects with autocommit disabled. After a successful FedAuth login, such clients send a TransactionManager (0x0E) request to begin a transaction. The mock does not recognize packet type 0x0E, so it silently drops the request and never replies — the client then blocks indefinitely waiting for the transaction response.

This makes the mock unusable for driver integration tests on Linux, where connect/login timeouts are not always honored against a half-responsive server, so the client hangs forever instead of erroring out.

Impact

We are adding mock-TDS FedAuth regression tests to mssql-python (regressions like microsoft/mssql-python#596). The tests hang on Linux CI (hit the 1-hour worker timeout). They only "pass" on Windows because there the ODBC login timeout is honored, so the still-open connection is eventually torn down.

Both mssql-python and pyodbc reproduce this — because Python DB-API drivers default to autocommit = OFF, so the ODBC Driver 18 begins a transaction immediately after login and issues a TransactionManager request.

Root cause

mssql-mock-tds/src/protocol.rs — the PacketType enum / TryFrom<u8> does not include 0x0E (TransactionManager). In server.rs::process_packet, PacketHeader::parse therefore fails, process_packet returns Ok(None), and no response is written. The read loop then blocks on the next read_buf, and the client blocks waiting for the transaction reply.

Evidence (mock trace log, TDS 7.4 / optional TLS, ODBC Driver 18 on Linux)

INFO  ...server: TLS handshake successful for 127.0.0.1:56527
DEBUG ...server: Handling Login7 from 127.0.0.1:56527
DEBUG ...protocol: Parsed access token from Login7: 36 bytes
DEBUG ...server: Stored access token (36 bytes) from 127.0.0.1:56527 for verification
DEBUG ...server: Sending 118 encrypted bytes response          # LoginAck + FeatureExtAck + Done
DEBUG ...server: Received 34 encrypted bytes from 127.0.0.1:56527 (TDS-wrapped TLS)
WARN  ...server: Failed to parse packet header: Invalid packet type: 14   # 14 == 0x0E TransactionManager
# ... no response sent; client blocks until it is force-closed 14s later
DEBUG ...server: TLS connection closed by client 127.0.0.1:56527

The FedAuth token is parsed correctly; the connection simply never completes because the TransactionManager request goes unanswered.

Requested change

  1. Handle TransactionManager (0x0E) requests. Add TransactionManager = 0x0E to PacketType and its TryFrom<u8>, and add a process_packet arm that:

    • Parses the RequestType (u16 LE) that follows the leading ALL_HEADERS block (u32 LE total length + that many header bytes).
    • Replies with an appropriate token stream so the client can proceed, e.g.:
      • TM_BEGIN_XACT (5) → ENVCHANGE type 8 (Begin Transaction) carrying an 8-byte transaction descriptor + DONE.
      • TM_COMMIT_XACT (7) → ENVCHANGE type 9 (Commit) + DONE.
      • TM_ROLLBACK_XACT (8) → ENVCHANGE type 10 (Rollback) + DONE.
      • anything else → DONE.
    • Wrap the tokens in a TabularResult (0x04) packet, like the Login7/SqlBatch paths do.

    This lets an autocommit-off connection complete its BEGIN TRAN handshake, so connect() returns successfully on all platforms and the connection closes cleanly.

  2. (Optional, nice-to-have) Record received token as soon as it is parsed. Today ConnectionStore is only populated after the per-connection read loop exits (i.e. after the client disconnects). Storing the token into the shared store at Login7 time would let tests observe has_received_token() even while the connection is still open, and makes the store resilient to clients that keep the connection alive.

Repro

  • Mock: TDS 7.4 optional-TLS mode (--tls-mode optional --cert ... --key ..., or PyMockTdsServer(port=0, tls=True)).
  • Client: mssql-python (or pyodbc) with ODBC Driver 18 on Linux, connecting with an access token via SQL_COPT_SS_ACCESS_TOKEN and default autocommit (OFF). The connect call never returns.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions