Skip to content

fix: preserve connection contexts in SQL driver wrappers - #1161

Open
strobil wants to merge 3 commits into
GoogleCloudPlatform:mainfrom
strobil:codex/preserve-driver-context
Open

strobil wants to merge 3 commits into
GoogleCloudPlatform:mainfrom
strobil:codex/preserve-driver-context

Conversation

@strobil

@strobil strobil commented Sep 10, 2026

Copy link
Copy Markdown

Why

The drivers registered by mysql.RegisterDriver, pgxv5.RegisterDriver (and the deprecated pgxv4 package) and mssql.RegisterDriver implement driver.Driver but not driver.DriverContext. sql.Open therefore wraps them in database/sql's internal dsnConnector, whose Connect(_ context.Context) discards the caller's context and calls Driver.Open(dsn).

Each wrapper then establishes the connection on a context unrelated to the caller:

  • pgxv5: stdlib.Driver.Open uses context.WithTimeout(context.Background(), 60*time.Second).
  • mysql: MySQLDriver.Open calls Connect(context.Background()).
  • mssql: the wrapper itself calls c.Connect(context.Background()).

database/sql reserves a pool slot (db.numOpen++) before calling Connect and releases it only after Connect returns. So when connection establishment is slow (for example, the SQL Admin API is slow or unreachable and Dialer.Dial is waiting for connection info), the slot stays occupied even after the caller's context has expired. The wait inside the connector does honor ctx.Done(), but it never receives the caller's context.

The effect is bounded differently per driver:

Wrapper Upper bound on a stuck Connect without this fix
pgxv5 / pgxv4 60 s, hard-coded in stdlib.Driver.Open
mysql no driver-level bound unless the DSN sets timeout= (default 0)
mssql 15 s, the msdsn dial timeout default

For MySQL the wait is bounded only by whatever gives up further down the stack: the connector's internal RefreshTimeout (60 s) when the default refresh-ahead cache is waiting on the SQL Admin API, or OS-level TCP behavior for the dial to the instance. With WithLazyRefresh the refresh runs on the background context and has no connector-level bound at all.

With a bounded pool, a single request whose deadline was 100 ms can hold a slot for up to 60 s, and every request queued behind it fails on its own deadline. A one-slot pool, a fake SQL Admin API transport delaying each response by 600 ms and a request with a 100 ms deadline reproduces this deterministically: at 150 ms the pool still reports OpenConnections=1, InUse=1, the next request hits its deadline with WaitCount=1, and the first request returns only after ~1.2 s.

Note that sql.OpenDB(stdlib.GetConnector(...)) with the same Cloud SQL dialer does not have this problem, because pgx's connector receives the caller's context. Only the documented RegisterDriversql.Open path is affected. All three wrappers still behave this way in v1.25.2.

What Changed

Implement OpenConnector in the MySQL, pgxv5 and SQL Server wrappers and delegate connection establishment to the underlying context-aware connectors. Preserve the Cloud SQL dial configuration, legacy Open behavior, and the registered wrapper returned by DB.Driver() / Connector.Driver(). The deprecated pgxv4 package inherits the fix through its existing pgxv5 delegation.

For SQL Server this also means the DSN is parsed and mssqldb.NewConnector is called once per *sql.DB instead of once per connection.

Verification

A regression test exercises the public RegisterDriversql.OpenPingContext path with a blocked SQL Admin API transport. It covers cancellation and a one-second deadline for MySQL, pgxv5, the pgxv4 compatibility package and SQL Server.

With the original wrappers, all eight cases remain blocked beyond the two-second assertion window. With the fix, they return the expected context error and leave zero open/in-use pool slots while the API remains blocked. The test also verifies driver identity and invalid-DSN error propagation. It needs neither Google credentials nor a live Cloud SQL instance.

@strobil
strobil marked this pull request as ready for review September 10, 2026 16:15
@strobil
strobil requested a review from a team as a code owner September 10, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants