gastownhall/beads · error

db: Update %s: recompute is_blocked: %w

Error message

db: Update %s: recompute is_blocked: %w

What it means

This error wraps a failure of RecomputeIsBlockedInTx, which recalculates is_blocked on all issues affected by a status change during Update. It runs inside the same transaction so derived blocking state stays consistent; if the recompute SQL fails, the Update is aborted with this wrapped error.

Source

Thrown at internal/storage/domain/db/issue.go:324

	if statusChanging {
		newStatus := coerceStatus(updates["status"])
		oldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned
		newActive := newStatus != types.StatusClosed && newStatus != types.StatusPinned
		if oldActive != newActive {
			var (
				affectedIssues, affectedWisps []string
				aerr                          error
			)
			if opts.UseWispsTable {
				affectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeForWispInTx(ctx, r.runner, id)
			} else {
				affectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeInTx(ctx, r.runner, id)
			}
			if aerr != nil {
				return fmt.Errorf("db: Update %s: affected by status change: %w", id, aerr)
			}
			if err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {
				return fmt.Errorf("db: Update %s: recompute is_blocked: %w", id, err)
			}
		}
	}
	// Snapshot only after all derived blocked-state maintenance has completed.
	// The no-op early returns above wrote nothing and journal nothing.
	return issueops.RecordEventInTx(ctx, r.runner, issueops.EventUpdate, id, actor)
}

// CompareAndSetMetadataKey runs the SHARED compare-and-set body, unwrapped.
//
// It is the whole of this leg's implementation, and that is the point: the two
// store backends wrap the same function in their own transaction, so the third
// leg is a wrapper check rather than an independent vote — which is what the
// conformance contract's header says.
//
// It does NOT wrap the error the way its siblings above do, for the reason
// WalkDependencyTree gives: the body publishes storage.ErrNotFound and
// storage.ErrValidation as the role's own vocabulary, both classified by

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped cause (%w) for the underlying driver error (deadlock, timeout, cancellation).
  2. Increase the operation's context timeout if the recompute over many dependents timed out.
  3. Retry the update; the transactional rollback leaves state consistent.
  4. Verify dependency rows and schema integrity for affected issues.

Example fix

// before
ctx := context.Background()
store.Update(ctx, id, opts) // may die mid-recompute on deadline
// after
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := store.Update(ctx, id, opts); err != nil { /* inspect %w cause */ }
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil { return fmt.Errorf("context already done: %w", err) }

Try / catch

err := store.Update(ctx, id, opts)
if err != nil && strings.Contains(err.Error(), "recompute is_blocked") {
    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
    defer cancel()
    err = store.Update(ctx, id, opts)
}

Prevention

When it happens

Trigger: Calling Update with a status change where RecomputeIsBlockedInTx errors: SQL failure, deadlocked or killed transaction, connection loss, or context cancellation mid-recompute.

Common situations: Long recomputes over many dependents hitting context deadlines; concurrent updates contending on dependent rows; driver/connection errors during large batch updates; wisp vs issue table mismatches.

Related errors


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