gastownhall/beads · error

release checked close savepoint: %w

Error message

release checked close savepoint: %w

What it means

Beads throws this when 'RELEASE SAVEPOINT <name>' fails after a checked close. Releasing discards the savepoint once the trial has succeeded (or been rolled back); a failure here means the transaction or connection is in trouble, even though the close itself may have been applied. It is often reported together with a rollback error via errors.Join.

Source

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

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 {
		return "depends_on_issue_id", nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return "", fmt.Errorf("classify child-count target in issues: %w", err)
	}
	err = tx.QueryRowContext(ctx, "SELECT 1 FROM wisps WHERE id = ?", id).Scan(&found)
	if err == nil {
		return "depends_on_wisp_id", nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat as a transaction-level failure: retry the entire close operation on a fresh transaction
  2. Look for an earlier joined error (errors.Join output) — the first error usually identifies the root cause
  3. Check backend/driver support for RELEASE SAVEPOINT semantics; upgrade driver if incomplete
  4. Investigate recurring deadlocks/lock timeouts if this happens under concurrent closes
Defensive patterns

Strategy: retry

Validate before calling

// Check transaction liveness before close-policy operations
if err := tx.QueryRowContext(ctx, "SELECT 1").Scan(&one); err != nil {
	return fmt.Errorf("transaction not viable for checked close: %w", err)
}

Try / catch

err := CloseIssueCheckedInTx(ctx, tx, id, opts)
if err != nil {
	if dberrors.IsConnectionError(err) || dberrors.IsDeadlock(err) {
		// savepoint may have been implicitly destroyed — restart on a fresh tx
		return retryCloseOnNewTx(ctx, id)
	}
	return err
}

Prevention

When it happens

Trigger: CloseIssueCheckedInTx or rollbackAndReleaseCloseCheckedSavepoint calling RELEASE SAVEPOINT when the transaction was already rolled back (deadlock/timeout), the savepoint was already released, or the connection failed after the close UPDATE succeeded.

Common situations: Engine implicitly rolled back the transaction after a prior statement error; connection drop between the close UPDATE and RELEASE; driver that auto-releases savepoints on statement error.

Related errors


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