gastownhall/beads · critical
discard checked close savepoint after %v: %w
Error message
discard checked close savepoint after %v: %w
What it means
After a checked close failed inside its savepoint, the cleanup (ROLLBACK TO SAVEPOINT + RELEASE SAVEPOINT) also failed. The library discards the original error's visibility and surfaces this compound error, because an un-released savepoint would corrupt the surrounding transaction's state. The message embeds the original scoped error (%v) and the cleanup error (%w).
Source
Thrown at internal/storage/issueops/close.go:95
return nil, err
}
if !found {
return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
}
// domain/db also supplies a *sql.DB Runner, whose independently pooled
// 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 falseView on GitHub (pinned to 71377f2769)
Solutions
- Treat the transaction as unusable: roll back the whole transaction and start over
- Inspect both the embedded scoped error (%v prefix) and the cleanup cause (%w) for the root failure
- Retry the close in a fresh transaction after connectivity is restored
- If contexts cancel frequently, ensure the context passed to the close outlives the operation
Example fix
err := store.CloseIssue(ctx, id, reason, actor)
if err != nil && strings.Contains(err.Error(), "discard checked close savepoint") {
// transaction is poisoned — abandon it entirely
tx.Rollback()
tx, err = db.BeginTx(ctx, nil)
// retry close in the fresh transaction
} Defensive patterns
Strategy: try-catch
Try / catch
err := store.CloseIssue(ctx, id, reason, actor)
if err != nil && strings.Contains(err.Error(), "discard checked close savepoint") {
// transaction is poisoned: roll back everything, do not commit
_ = tx.Rollback()
return retryInFreshTransaction(ctx, id)
} Prevention
- Never commit a transaction after savepoint cleanup failed
- Use savepoint-eligible handles (*sql.Tx or *sql.Conn), not pooled *sql.DB runners
- Ensure contexts outlive the close operation; monitor connection health
When it happens
Trigger: closeIssueCheckedAfterSavepoint returned an error AND rollbackAndReleaseCloseCheckedSavepoint also failed — typically because the transaction/connection is dead (server gone, connection closed) so even ROLLBACK TO SAVEPOINT cannot execute.
Common situations: Dolt server connection dropped during the close; transaction already marked failed by a prior SQL error the driver treats as aborting; context canceled mid-close killing both the statement and the cleanup.
Related errors
- release checked close savepoint: %v; rollback to savepoint:
- 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/0236307953881f11.
Report an issue: GitHub.