gastownhall/beads · critical
release checked close savepoint: %v; rollback to savepoint:
Error message
release checked close savepoint: %v; rollback to savepoint: %w
What it means
The checked close itself succeeded, but RELEASE SAVEPOINT failed, and the fallback ROLLBACK TO SAVEPOINT also failed. The close work may be in an ambiguous savepoint state inside the transaction, so the library returns this compound error (release error as %v context, rollback error wrapped via %w) instead of pretending the transaction is clean.
Source
Thrown at internal/storage/issueops/close.go:101
// statements cannot retain a savepoint. The shared UOW uses a pinned
// *sql.Conn after START TRANSACTION, while embedded callers use *sql.Tx.
if !closeCheckedSavepointEligible(tx) {
return closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)
}
savepoint, err := createCloseCheckedSavepoint(ctx, tx)
if err != nil {
return nil, err
}
result, scopedErr := closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)
if scopedErr != nil {
if cleanupErr := rollbackAndReleaseCloseCheckedSavepoint(ctx, tx, savepoint); cleanupErr != nil {
return nil, fmt.Errorf("discard checked close savepoint after %v: %w", scopedErr, cleanupErr)
}
return nil, scopedErr
}
if err := releaseCloseCheckedSavepoint(ctx, tx, savepoint); err != nil {
if rollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, savepoint); rollbackErr != nil {
return nil, fmt.Errorf("release checked close savepoint: %v; rollback to savepoint: %w", err, rollbackErr)
}
return nil, err
}
return result, nil
}
func closeCheckedSavepointEligible(tx DBTX) bool {
switch tx.(type) {
case *sql.Tx, *sql.Conn:
return true
default:
return false
}
}
func closeIssueCheckedAfterSavepoint(ctx context.Context, tx DBTX, id, reason, actor, session string, force, closed bool, targetColumn string) (*CloseResult, error) {
openChildren, err := enforceClosePolicyForTargetInTx(ctx, tx, id, targetColumn, force, closed)
if err != nil {View on GitHub (pinned to 71377f2769)
Solutions
- Roll back the entire transaction; do not attempt to commit it
- Read the embedded release error for the root cause (connection/context)
- Retry the close in a new transaction — the previous one is discarded
- If using a custom DBTX wrapper, ensure it is *sql.Tx or *sql.Conn so savepoint logic is eligible, or avoid the savepoint path entirely
Example fix
if err != nil && strings.Contains(err.Error(), "rollback to savepoint") {
_ = tx.Rollback() // do not commit — savepoint state is unknown
return retryCloseInFreshTx(ctx, id)
} Defensive patterns
Strategy: try-catch
Try / catch
result, err := store.CloseIssue(ctx, id, reason, actor)
if err != nil && strings.Contains(err.Error(), "rollback to savepoint") {
_ = tx.Rollback() // savepoint state unknown — abandon the transaction
return retryInFreshTransaction(ctx, id)
} Prevention
- Treat any savepoint error as transaction-fatal; never commit afterward
- Keep connections alive (avoid mid-operation network churn and context cancels)
- Pass real *sql.Tx/*sql.Conn handles so the eligible fast path avoids savepoints only when correct
When it happens
Trigger: createCloseCheckedSavepoint succeeded and the close completed, but both RELEASE SAVEPOINT and ROLLBACK TO SAVEPOINT error — nearly always a dead connection, canceled context, or a transaction the driver has already aborted.
Common situations: Server restart or network partition right after the close statement; a *sql.DB Runner passed where a savepoint-capable *sql.Tx/*sql.Conn is expected (that path is normally gated by closeCheckedSavepointEligible, but custom DBTX wrappers can evade it).
Related errors
- discard checked close savepoint after %v: %w
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7d142802ca085d70.
Report an issue: GitHub.