go-transaction-manager 2.0.4 Fixes pgx Rollback Concurrency
go-transaction-manager is a Go transaction boundary library for database drivers, and this week its useful change is narrow but important. The maintainers changed pgx v4 and pgx v5 rollback behavior so context cancellation no longer starts a background rollback that can touch pgx.Tx while a query is still running.
The pgx rollback race is gone
The main patch removes awaitDone from both pgx drivers. Before this change, pgx transactions started a goroutine that watched ctx.Done() and called Rollback(ctx) when the context was canceled. That looked convenient, but it also meant a rollback could run while user code still had a query in progress on the same transaction.
That matters because pgx.Tx is not safe for concurrent use. The commit message calls out a real failure mode: a panic with BUG: slow write timer already active. This is the sort of bug that can sit unnoticed in normal tests, then appear under a slow query, a large write, and a timeout. Nice way to ruin an otherwise boring incident.
The code change is small in drivers/pgxv4/transaction.go and drivers/pgxv5/transaction.go. Both drivers drop the sync.Mutex, remove the background watcher, and add comments saying that a query must not overlap with Commit or Rollback on the same transaction.
awaitDoneis gone from pgx v4awaitDoneis gone from pgx v5CommitandRollbackno longer take a local mutex- The package comments now say standalone callers must call
Rollbackthemselves
Manager users get the quiet path
For users who run transactions through manager.Manager, the new behavior should be boring in the good sense. The manager still owns the close path. It calls rollback after the transactional function returns, so a cancel signal does not race a still running query inside the function body.
The important part is ordering. With the manager path, user code returns from the callback first. Then the manager performs cleanup. That keeps the transaction boundary in one place and avoids another goroutine making a second call into pgx while the first one is still writing to the connection.
That is also why this is more than a cosmetic refactor. It changes the failure shape from “a cancel can trigger rollback behind your back” to “the transaction is cleaned up at the managed boundary.” For most service code that already uses the manager API, this should reduce surprise without requiring a local code change.
Standalone pgx callers need an audit
The cost sits with standalone users of the pgx driver factory. The changelog now says cancellation does not roll back a transaction from a background goroutine. If you create a transaction and keep the returned Transaction yourself, your code must call Rollback when it chooses not to commit.
The README warning in README.md is clear on this point. Do not run a query at the same time as Commit or Rollback on the same pgx.Tx. Do not assume a canceled context will clean up a standalone transaction for you. The explicit cleanup rule is less magical, but it is easier to reason about when pgx itself cannot make concurrent transaction calls safe.
A simple audit target is any code path that creates a pgx transaction outside manager.Manager and then returns on ctx.Err(). Those paths should make the cleanup call visible.
ctx, tx, err := factory(ctx, settings.Must())
if err != nil {
return err
}
defer func() {
_ = tx.Rollback(context.Background())
}()
Do not copy that blindly into code that already delegates cleanup to the manager. The point is ownership. Pick one owner for the transaction close path, then keep it there.
Tests now hit the actual failure mode
The useful part of the test change is that it tries to reproduce the race with a real database. Both drivers/pgxv4/transaction_integration_test.go and drivers/pgxv5/transaction_integration_test.go add a test named TestTransaction_WithRealDB_NoDataRaceOnContextCancelDuringQuery_139.
The test uses manager.Must(...), starts a query that sleeps, sends an 8 MB string parameter, and cancels the context during execution. That large parameter is not random noise. It keeps the connection write path busy, which gives the old rollback goroutine a better chance to collide with the in progress query.
The Makefile also changed so the common test targets disable the Go test cache. The same patch updates Makefile with -count=1 for normal, real database, and coverage test targets. For a timing sensitive regression, cached success is not much comfort.
make test.with_real_db
There is one small spillover into the SQL and sqlx tests. Their wait before checking context cancellation moved from one millisecond to one hundred milliseconds. That is not the headline change, but it makes those tests less twitchy when scheduler timing is not friendly.
What to watch
The follow up commit fixes the changelog tag from 2.0.3 to 2.0.4. That makes CHANGELOG.md match the current pgx concurrency fix instead of leaving readers with the wrong version header.
For project users, the practical checklist is short.
- If you use
manager.Manager, confirm your transaction work stays inside the callback and let the manager close it - If you use pgx transactions standalone, add explicit rollback on every path that does not commit
- If you run real database tests locally, prefer the Makefile targets so
-count=1is applied
The change is mostly about removing hidden behavior. That is the right direction for pgx transaction code. A rollback that happens later is annoying. A rollback that happens at the same time as a query is worse.