{"record":{"id":"5d55e6fe1df3ffc3","repo":"gastownhall/beads","slug":"affected-by-close-for-s-w","errorCode":null,"errorMessage":"affected by close for %s: %w","messagePattern":"affected by close for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":330,"sourceCode":"\t\treturn false, \"\", false, fmt.Errorf(\"read status from %s: %w\", target.table, err)\n\t}\n\treturn false, \"\", false, nil\n}\n\n//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)\nfunc closeIssueInTx(ctx context.Context, tx DBTX, id string, reason, actor, session string, recordEvent bool) (*CloseResult, error) {\n\tisWisp := IsActiveWispInTx(ctx, tx, id)\n\tissueTable, _, eventTable, _ := WispTableRouting(isWisp)\n\n\tvar affectedIssues, affectedWisps []string\n\tvar aerr error\n\tif isWisp {\n\t\taffectedIssues, affectedWisps, aerr = AffectedByStatusChangeForWispInTx(ctx, tx, id)\n\t} else {\n\t\taffectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)\n\t}\n\tif aerr != nil {\n\t\treturn nil, fmt.Errorf(\"affected by close for %s: %w\", id, aerr)\n\t}\n\n\tnow := time.Now().UTC()\n\n\t// row_lock is rewritten on close so a concurrent reclaim (which also rewrites\n\t// row_lock) collides on this cell and is forced to conflict-and-retry rather\n\t// than silently cell-merging a revert-to-ready over a completed close (see\n\t// lease.go). The lease row is deleted below: a closed issue holds no lease.\n\tresult, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\tUPDATE %s SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?,\n\t\t\trow_lock = ?\n\t\tWHERE id = ? AND status != ?\n\t`, issueTable), types.StatusClosed, now, now, reason, session, freshRowLock(), id, types.StatusClosed)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to close issue: %w\", err)\n\t}\n\n\trows, err := result.RowsAffected()","sourceCodeStart":312,"sourceCodeEnd":348,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L312-L348","documentation":"Beads throws this when computing which issues would be affected by closing the given id fails. closeIssueInTx calls AffectedByStatusChangeInTx (or the wisp variant) before performing the close UPDATE; if that impact analysis errors, the close is aborted with the id embedded for debugging. The root driver error is wrapped underneath.","triggerScenarios":"CloseIssue / CloseIssueWithoutEventInTx → closeIssueInTx when AffectedByStatusChangeInTx or AffectedByStatusChangeForWispInTx returns an error: dependency-graph queries hitting connection failure, lock timeout, missing dependencies/wisp_dependencies tables (when not gated as optional), or context cancellation while traversing the graph.","commonSituations":"Large dependency graphs making impact queries slow enough to hit timeouts; concurrent bulk operations contending on dependencies rows; partially provisioned wisp tables; database connectivity loss between the impact query and the close.","solutions":["Inspect the wrapped cause after 'affected by close for <id>:' — it chains the original driver error","Retry the close if the cause was transient; the whole close is transactional and retry-safe","Run schema/migration checks if a dependencies or wisp_dependencies table is reported missing","Reduce lock contention (avoid running bulk ops concurrently with closes) and add generous context timeouts for large graphs"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-check the dependency tables the impact query needs\nfor _, t := range []string{\"dependencies\", \"wisp_dependencies\"} {\n\tvar one int\n\terr := db.QueryRow(\"SELECT 1 FROM \" + t + \" LIMIT 1\").Scan(&one)\n\tif err != nil && !dberrors.IsTableNotExist(err) {\n\t\treturn fmt.Errorf(\"%s unreadable before close: %w\", t, err)\n\t}\n}","typeGuard":null,"tryCatchPattern":"res, err := CloseIssue(ctx, id, opts)\nif err != nil && strings.Contains(err.Error(), \"affected by close for\") {\n\tif dberrors.IsTransient(err) || errors.Is(err, context.DeadlineExceeded) {\n\t\treturn retryCloseWithBackoff(ctx, id)\n\t}\n\treturn fmt.Errorf(\"impact analysis failed for %s: %w\", id, err)\n}","preventionTips":["Add context timeouts proportional to dependency-graph depth before closing hub issues","Serialize bulk dependency mutations with issue closes to avoid lock contention","Keep dependencies and wisp_dependencies schemas fully migrated","Retry closes on transient errors; the transactional design makes retries safe"],"tags":["sql","dependency-tracking","transaction"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}