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 false

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat the transaction as unusable: roll back the whole transaction and start over
  2. Inspect both the embedded scoped error (%v prefix) and the cleanup cause (%w) for the root failure
  3. Retry the close in a fresh transaction after connectivity is restored
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/0236307953881f11. Report an issue: GitHub.