Transaction Without Rollback Path
ID |
go.database_transactions_properly_handled_rollback |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Go |
Tags |
reliability, resource-leak |
Description
Reports a function that opens a database transaction and commits it without ever providing a
rollback path — no defer tx.Rollback(), and no tx.Rollback() call anywhere in the function.
The check is function-scoped and single-file. A begin call is reported only when the same function
also commits: that is the corroboration that the Begin-named call really opened a transaction,
so an unrelated Begin — on a parser, a span, a batch builder — never fires. It also exempts, by
construction, the handover shape where the transaction is returned to the caller, since such a
function does not commit and the caller owns the rollback.
Rationale
Between Begin and Commit, every statement can fail. When one does and the function returns the
error, the transaction is neither committed nor rolled back: it stays open, holding its locks and
its connection from the pool, until the driver eventually recycles the connection.
One leaked transaction is invisible. Under load they accumulate: the pool drains, unrelated queries start blocking on locks the abandoned transactions still hold, and the service stalls in a way that looks like a database problem rather than a missing line of Go.
func createUser(db *sql.DB, name string) error {
tx, err := db.Begin() // FLAW — no rollback path anywhere in the function
if err != nil {
return err
}
if _, err := tx.Exec("insert into users(name) values (?)", name); err != nil {
return err // transaction left open, holding locks
}
return tx.Commit()
}
Remediation
Defer the rollback immediately after a successful begin. Once the commit has succeeded the deferred rollback is a no-op, so the single line covers every error path, including a panic.
func createUser(db *sql.DB, name string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no-op once Commit has succeeded
if _, err := tx.Exec("insert into users(name) values (?)", name); err != nil {
return err
}
return tx.Commit()
}
Configuration
| Property | Default | Description |
|---|---|---|
|
|
Method names that open a transaction. Add an ORM’s own name to have the rule follow it. |
|
|
Method names that commit a transaction. A begin is reported only when the function also commits. |
|
|
Method names that abort a transaction. Any one of them, anywhere in the function, clears the finding. |