{"record":{"id":"46c6af74ea53807a","repo":"gastownhall/beads","slug":"db-update-s-recompute-is-blocked-w","errorCode":null,"errorMessage":"db: Update %s: recompute is_blocked: %w","messagePattern":"db: Update (.+?): recompute is_blocked: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":324,"sourceCode":"\tif statusChanging {\n\t\tnewStatus := coerceStatus(updates[\"status\"])\n\t\toldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned\n\t\tnewActive := newStatus != types.StatusClosed && newStatus != types.StatusPinned\n\t\tif oldActive != newActive {\n\t\t\tvar (\n\t\t\t\taffectedIssues, affectedWisps []string\n\t\t\t\taerr                          error\n\t\t\t)\n\t\t\tif opts.UseWispsTable {\n\t\t\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeForWispInTx(ctx, r.runner, id)\n\t\t\t} else {\n\t\t\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeInTx(ctx, r.runner, id)\n\t\t\t}\n\t\t\tif aerr != nil {\n\t\t\t\treturn fmt.Errorf(\"db: Update %s: affected by status change: %w\", id, aerr)\n\t\t\t}\n\t\t\tif err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {\n\t\t\t\treturn fmt.Errorf(\"db: Update %s: recompute is_blocked: %w\", id, err)\n\t\t\t}\n\t\t}\n\t}\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// The no-op early returns above wrote nothing and journal nothing.\n\treturn issueops.RecordEventInTx(ctx, r.runner, issueops.EventUpdate, id, actor)\n}\n\n// CompareAndSetMetadataKey runs the SHARED compare-and-set body, unwrapped.\n//\n// It is the whole of this leg's implementation, and that is the point: the two\n// store backends wrap the same function in their own transaction, so the third\n// leg is a wrapper check rather than an independent vote — which is what the\n// conformance contract's header says.\n//\n// It does NOT wrap the error the way its siblings above do, for the reason\n// WalkDependencyTree gives: the body publishes storage.ErrNotFound and\n// storage.ErrValidation as the role's own vocabulary, both classified by","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L306-L342","documentation":"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.","triggerScenarios":"Calling Update with a status change where RecomputeIsBlockedInTx errors: SQL failure, deadlocked or killed transaction, connection loss, or context cancellation mid-recompute.","commonSituations":"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.","solutions":["Check the wrapped cause (%w) for the underlying driver error (deadlock, timeout, cancellation).","Increase the operation's context timeout if the recompute over many dependents timed out.","Retry the update; the transactional rollback leaves state consistent.","Verify dependency rows and schema integrity for affected issues."],"exampleFix":"// before\nctx := context.Background()\nstore.Update(ctx, id, opts) // may die mid-recompute on deadline\n// after\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\nif err := store.Update(ctx, id, opts); err != nil { /* inspect %w cause */ }","handlingStrategy":"retry","validationCode":"if err := ctx.Err(); err != nil { return fmt.Errorf(\"context already done: %w\", err) }","typeGuard":null,"tryCatchPattern":"err := store.Update(ctx, id, opts)\nif err != nil && strings.Contains(err.Error(), \"recompute is_blocked\") {\n    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n    defer cancel()\n    err = store.Update(ctx, id, opts)\n}","preventionTips":["Set timeouts proportional to the issue's dependent count.","Batch status changes during low-contention windows.","Run bd doctor periodically to catch schema drift early.","Avoid concurrent bulk updates touching the same dependency graph."],"tags":["database","transaction","blocked-state"],"backgroundTag":"db-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}