gastownhall/beads · error

recompute is_blocked after delete for %s: %w

Error message

recompute is_blocked after delete for %s: %w

What it means

DeleteIssueInTx wraps a failure from RecomputeIsBlockedInTx, the step that recalculates the is_blocked flag for every issue/wisp affected by the deletion. The issue row itself was already deleted at this point, but everything is in one transaction, so the whole delete rolls back and the issue still exists.

Source

Thrown at internal/storage/issueops/delete.go:47

	} else {
		deletedIssues = []string{id}
	}
	affectedIssues, affectedWisps, aerr := AffectedByDeletionInTx(ctx, tx, deletedIssues, deletedWisps)
	if aerr != nil {
		return fmt.Errorf("affected by delete for %s: %w", id, aerr)
	}

	// Edges are journaled before the rows go, while their source snapshots can
	// still be read.
	if err := RecordDependencyRemovalsForIssuesInTx(ctx, tx, []string{id}); err != nil {
		return fmt.Errorf("journal dependency removals for %s: %w", id, err)
	}
	if err := deleteIssueRowInTx(ctx, tx, id, isWisp); err != nil {
		return err
	}

	if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
		return fmt.Errorf("recompute is_blocked after delete for %s: %w", id, err)
	}

	return nil
}

//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func deleteIssueRowInTx(ctx context.Context, tx *sql.Tx, id string, isWisp bool) error {
	issueTable, _, _, _ := WispTableRouting(isWisp)
	result, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s WHERE id = ?", issueTable), id)
	if err != nil {
		return fmt.Errorf("delete issue from %s: %w", issueTable, err)
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("get rows affected: %w", err)
	}
	if rows == 0 {
		// Wrap the sentinel so callers can errors.Is(..., storage.ErrNotFound),

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the delete in a fresh transaction — rollback guarantees the issue was not actually removed.
  2. Increase the context timeout when deleting issues with large dependent graphs.
  3. Check the wrapped error for lock-wait-timeout and reduce concurrent writes to the same subgraph.
  4. Verify the schema is current (stale schema can make the recompute statement fail).
Defensive patterns

Strategy: retry

Validate before calling

// estimate affected subgraph before deleting
deps, err := externalDependentsOf(ctx, db, id)
if err == nil && len(deps) > 500 { /* use a longer context for the delete */ }

Try / catch

if err := storage.DeleteIssue(ctx, db, id); err != nil {
	if errors.Is(err, context.DeadlineExceeded) || isLockTimeout(err) {
		// issue still exists (tx rolled back); retry with bigger timeout
	}
	return err
}

Prevention

When it happens

Trigger: Calling DeleteIssue/DeleteIssueInTx when the is_blocked recompute UPDATE fails — SQL error on the affected-issues set, context canceled, or lost connection after the row delete.

Common situations: Very large affected-issues set (deleting a hub issue with many dependents) causing a slow recompute that exceeds the statement/context timeout; concurrent writers contending on rows being recomputed.

Related errors


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