{"record":{"id":"e58f4e73a563d7c7","repo":"gastownhall/beads","slug":"db-dependencysqlrepository-delete-recompute-is-b","errorCode":null,"errorMessage":"db: DependencySQLRepository.Delete: recompute is_blocked: %w","messagePattern":"db: DependencySQLRepository\\.Delete: recompute is_blocked: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/dependency.go","lineNumber":384,"sourceCode":"\t\t\tNewValue: fmt.Sprintf(\"Removed dependency on %s\", dependsOnID),\n\t\t}, domain.RecordEventOpts{UseWispsTable: opts.UseWispsTable}); err != nil {\n\t\t\treturn domain.DepDeleteResult{}, fmt.Errorf(\"db: DependencySQLRepository.Delete: record dependency_removed event: %w\", err)\n\t\t}\n\t}\n\n\tdt := types.DependencyType(depType)\n\tvar affectedIssues, affectedWisps []string\n\tvar aerr error\n\tif opts.UseWispsTable {\n\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByDepChangeForWispInTx(ctx, r.runner, issueID, dependsOnID, dt)\n\t} else {\n\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByDepChangeInTx(ctx, r.runner, issueID, dependsOnID, dt)\n\t}\n\tif aerr != nil {\n\t\treturn domain.DepDeleteResult{}, fmt.Errorf(\"db: DependencySQLRepository.Delete: affected set: %w\", aerr)\n\t}\n\tif err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {\n\t\treturn domain.DepDeleteResult{}, fmt.Errorf(\"db: DependencySQLRepository.Delete: recompute is_blocked: %w\", err)\n\t}\n\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// Never gated on opts.EmitEvent — a structural removal is as real to a\n\t// replaying consumer as one from an explicit dep verb.\n\tif err := issueops.RecordDepEventInTx(ctx, r.runner, issueops.EventDepRemove, issueID, depType, dependsOnID, depMetadata, actor); err != nil {\n\t\treturn domain.DepDeleteResult{}, err\n\t}\n\n\treturn domain.DepDeleteResult{Found: true, Type: dt, DependsOnID: dependsOnID}, nil\n}\n\nfunc (r *dependencySQLRepositoryImpl) HasCycle(ctx context.Context, issueID, dependsOnID string) (bool, error) {\n\tif issueID == \"\" || dependsOnID == \"\" {\n\t\treturn false, errors.New(\"db: DependencySQLRepository.HasCycle: issueID and dependsOnID must not be empty\")\n\t}\n\n\tcycle, err := issueops.WouldCreateSchedulingCycleInTx(ctx, r.runner, issueID, dependsOnID, nil)","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/dependency.go#L366-L402","documentation":"Wraps a failure from RecomputeIsBlockedInTx, which refreshes the is_blocked flag for every issue/wisp in the affected set after a dependency deletion. The library aborts the Delete with this error because leaving is_blocked stale would corrupt readiness computation ('ready' work queues) for all consumers.","triggerScenarios":"Delete deletes the edge and computes the affected set fine, but the batch UPDATE recomputing is_blocked fails: DB write error, one of the affected issue IDs no longer exists (FK/row-missing error in strict mode), connection drop, or context cancellation.","commonSituations":"Concurrent deletion of an issue in the affected set racing the recompute; Dolt transaction conflict under concurrent writers; oversized affected set hitting packet/lock limits; connection pool exhausted.","solutions":["Retry the Delete; if the edge row is already gone, the call returns Found:false and you can re-run RecomputeIsBlocked for the affected issues manually if needed.","Serialize concurrent writers on the same issue graph (per-issue locking in the caller) to avoid conflicts.","Check for missing referenced issues (deleted concurrently) and ensure soft-delete flow rather than hard delete.","Verify DB transaction/lock health; reduce batch size by splitting large affected sets if the driver errors on huge statements."],"exampleFix":"// before\nres, err := deps.Delete(ctx, issueID, depID, actor, opts)\nif err != nil { log.Fatal(err) } // stale is_blocked risk unexamined\n// after\nres, err := deps.Delete(ctx, issueID, depID, actor, opts)\nif err != nil {\n    if strings.Contains(err.Error(), \"recompute is_blocked\") {\n        // retry or manually recompute for affected issues\n    }\n}","handlingStrategy":"retry","validationCode":"if err := ctx.Err(); err != nil { return err }","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"recompute is_blocked\") {\n    // edge may already be deleted; retry is safe (Found:false), or manually\n    // recompute is_blocked for the affected issues via the recomputation helper\n}","preventionTips":["Serialize writers on the same issue graph to avoid recompute conflicts.","Prefer soft-delete of issues so affected-set recompute never hits missing rows.","Retry transient failures — the operation is idempotent.","Watch Dolt transaction-conflict logs under concurrent write load."],"tags":["database","dependency","consistency"],"backgroundTag":"dependency-recompute-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}