Skip to content

FEAT: Built-in configurable connection and transient-fault retry logic #682

Description

@dlevy-msft-sql

Summary

Add first-class, configurable retry logic to mssql-python so applications get transient-fault resiliency without hand-rolling their own retry loops. Today the only retry surface is the ODBC-level ConnectRetryCount/ConnectRetryInterval keywords, which only silently reconnect a dropped idle connection. They do not retry a connect() that fails transiently, nor a query that fails with a recoverable error (deadlock victim, lock/query timeout, or Azure SQL throttling such as 40197/40501/49918). Every app has to reimplement this, and most get the backoff, jitter, and "which errors are retriable" classification subtly wrong.

Motivation

  • Parity with the .NET Microsoft.Data.SqlClient configurable retry providers (SqlRetryLogicBaseProvider, SqlConnection.RetryLogicProvider / SqlCommand.RetryLogicProvider) and the Azure SDK retry-policy conventions.
  • Azure SQL Database and SQL Managed Instance routinely surface transient errors during failover, scaling, and throttling; robust handling is effectively required for production.
  • Reduces copy-pasted, error-prone retry loops and centralizes the transient-error taxonomy in the driver where it can be maintained authoritatively.

Proposed API (for discussion)

A retry policy object, attachable at the connection level and overridable per cursor/execute:

from mssql_python import RetryPolicy, connect

policy = RetryPolicy(
    max_attempts=3,           # total tries, not extra retries
    backoff="exponential",    # "exponential" | "fixed" | "none"
    base_delay=1.0,           # seconds
    max_delay=30.0,           # cap per wait
    jitter=True,              # full jitter to avoid thundering herd
    # Retriable classification, defaulted by the driver but overridable:
    # connect-scope errors -> fresh connection; query-scope -> same connection
)

conn = connect(conn_str, retry_policy=policy)      # applies to connect() + queries
cursor = conn.cursor(retry_policy=policy)          # optional override
cursor.execute(sql, *params, retry_policy=policy)  # optional per-call override

Key behaviors:

  • Two scopes, because recovery differs: connection-establishment/connection-loss errors need a fresh connection; connection-surviving errors (deadlock, query timeout, throttling) retry on the same connection.
  • Driver-maintained default retriable set (transient SQLSTATEs / native error numbers), overridable by the caller.
  • Idempotency guardrail: by default only retry statements the caller marks safe, or document clearly that writes must be wrapped in an explicit transaction. Never silently replay a non-idempotent write.
  • Observability: emit standard logging records on each retry (attempt count, error, delay) and on final give-up.
  • No behavior change by default (opt-in), so existing code is unaffected.

Alternatives considered

  • ODBC ConnectRetryCount/ConnectRetryInterval — only covers idle-connection reconnect, not connect() or query retries.
  • Application-level helper loops — what everyone does now; duplicative and easy to get wrong (backoff, jitter, error classification, connect-vs-query scope).

Additional context

Docs currently ship a sample connect_with_retry / execute_with_retry pattern that demonstrates exactly this behavior and would map cleanly onto a built-in RetryPolicy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionenhancementNew feature or requestinADOtriage doneIssues that are triaged by dev team and are in investigation.up for grabs 🙌Issues that are ready to be picked up for anyone interested. Please self-assign and remove the label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions