gastownhall/beads · error

rollback checked close savepoint: %w

Error message

rollback checked close savepoint: %w

What it means

Beads throws this when 'ROLLBACK TO SAVEPOINT <name>' fails after a checked close trial determined the close could not proceed (e.g. open children). Rolling back the savepoint undoes the trial-run writes; failing here leaves the transaction in an indeterminate state, so the caller joins this error with the release error and aborts.

Source

Thrown at internal/storage/issueops/close.go:251

func createCloseCheckedSavepoint(ctx context.Context, tx DBTX) (string, error) {
	name := closeCheckedSavepointPrefix + strconv.FormatUint(closeCheckedSavepointCounter.Add(1), 10)
	//nolint:gosec // G201: name is a fixed identifier-safe prefix plus an atomic decimal counter.
	if _, err := tx.ExecContext(ctx, "SAVEPOINT "+name); err != nil {
		return "", fmt.Errorf("create checked close savepoint: %w", err)
	}
	return name, nil
}

func rollbackAndReleaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {
	rollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, name)
	releaseErr := releaseCloseCheckedSavepoint(ctx, tx, name)
	return errors.Join(rollbackErr, releaseErr)
}

func rollbackToCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {
	//nolint:gosec // G201: name is generated by createCloseCheckedSavepoint.
	if _, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT "+name); err != nil {
		return fmt.Errorf("rollback checked close savepoint: %w", err)
	}
	return nil
}

func releaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {
	//nolint:gosec // G201: name is generated by createCloseCheckedSavepoint.
	if _, err := tx.ExecContext(ctx, "RELEASE SAVEPOINT "+name); err != nil {
		return fmt.Errorf("release checked close savepoint: %w", err)
	}
	return nil
}

// dependencyTargetColumnForIDInTx returns the typed target column for id,
// preferring a durable row when an id is present in both tables.
func dependencyTargetColumnForIDInTx(ctx context.Context, tx DBTX, id string) (string, error) {
	var found int
	err := tx.QueryRowContext(ctx, "SELECT 1 FROM issues WHERE id = ?", id).Scan(&found)
	if err == nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check for a preceding error in the same transaction (deadlock/lock timeout) — that original failure is the root cause; fix or retry for it
  2. Retry the whole close operation; transactional semantics make a full retry safe after a connection-level rollback
  3. Avoid nesting beads transactions with external savepoint management on the same connection
  4. If using a custom driver, verify it preserves savepoints after statement-level errors
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure no prior statement has poisoned the transaction before the trial
if err := tx.QueryRowContext(ctx, "SELECT 1").Scan(&one); err != nil {
	return fmt.Errorf("transaction already aborted, savepoint rollback will fail: %w", err)
}

Try / catch

if err := CloseIssueCheckedInTx(ctx, tx, id, opts); err != nil {
	var joined interface{ Unwrap() []error }
	if errors.As(err, &joined) {
		for _, sub := range joined.Unwrap() {
			if dberrors.IsDeadlock(sub) || dberrors.IsLockTimeout(sub) {
				return retryClose(ctx, id) // root cause was the earlier statement
			}
		}
	}
	return err
}

Prevention

When it happens

Trigger: CloseIssueCheckedInTx (or rollbackAndReleaseCloseCheckedSavepoint) invoking ROLLBACK TO SAVEPOINT when: the savepoint no longer exists (released or outer rollback already occurred), the transaction was already terminated by a prior error (e.g. deadlock or lock timeout), or the connection dropped mid-transaction.

Common situations: A prior statement in the transaction caused an implicit transaction rollback (deadlock, serialization failure), making the savepoint vanish; another layer released the same savepoint; connection pool reclaiming a broken connection.

Related errors


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