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
- Retry the delete in a fresh transaction — rollback guarantees the issue was not actually removed.
- Increase the context timeout when deleting issues with large dependent graphs.
- Check the wrapped error for lock-wait-timeout and reduce concurrent writes to the same subgraph.
- 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
- Delete hub issues (many dependents) with long timeouts or during low-write windows.
- Remember rollback semantics: a failure here leaves the issue intact.
- Reduce concurrent writers to the same dependency subgraph.
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
- failed to close issue: %w
- affected by delete for %s: %w
- journal dependency removals for %s: %w
- partition delete ids: %w
- recompute is_blocked after batch delete: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e3a276c981de579e.
Report an issue: GitHub.