{"record":{"id":"6d12a485a7771cec","repo":"gastownhall/beads","slug":"db-issuesqlrepository-wakeexpireddefers-w","errorCode":null,"errorMessage":"db: IssueSQLRepository.WakeExpiredDefers: %w","messagePattern":"db: IssueSQLRepository\\.WakeExpiredDefers: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":1263,"sourceCode":"\t\treturn fmt.Errorf(\"db: IssueSQLRepository.HeartbeatIssue: %w: %s is ephemeral\", storage.ErrNotClaimable, id)\n\t}\n\tif err := issueops.HeartbeatIssueInTx(ctx, r.runner, id, actor); err != nil {\n\t\treturn fmt.Errorf(\"db: IssueSQLRepository.HeartbeatIssue: %w\", err)\n\t}\n\treturn nil\n}\n\n// WakeExpiredDefers runs the shared lazy defer-wake body against this\n// repository's runner (the same DBTX-shaped seam ReclaimExpiredLeases uses)\n// and reports how many rows woke per table. The issues count decides whether\n// the transaction's owner mints a dolt commit; the wisps count decides\n// whether it must still issue a plain SQL commit — wisp tables are\n// dolt_ignored, so a wisp-only wake mints no version commit, but a caller\n// that treats it as \"nothing happened\" rolls the wisp writes back.\nfunc (r *issueSQLRepositoryImpl) WakeExpiredDefers(ctx context.Context) (issues, wisps int, err error) {\n\tout, err := issueops.WakeExpiredDefersInTx(ctx, r.runner)\n\tif err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"db: IssueSQLRepository.WakeExpiredDefers: %w\", err)\n\t}\n\treturn len(out.Issues), len(out.Wisps), nil\n}\n\nfunc (r *issueSQLRepositoryImpl) ReclaimExpiredLeases(ctx context.Context, olderThan time.Duration, filter types.ReclaimFilter, actor string) ([]types.ReclaimedLease, error) {\n\tcutoff := time.Now().UTC().Add(-olderThan)\n\tout, err := issueops.ReclaimExpiredLeasesInTx(ctx, r.runner, cutoff, filter, actor)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.ReclaimExpiredLeases: %w\", err)\n\t}\n\treturn out, nil\n}\n\nconst deleteBatchSize = 200\n","sourceCodeStart":1245,"sourceCodeEnd":1278,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L1245-L1278","documentation":"Wraps a failure from issueops.WakeExpiredDefersInTx, which scans for issues/wisps whose deferral has expired and moves them back to open, returning per-table counts. Any error in that multi-table sweep is wrapped with repository context. Note that a wisp-only wake still writes wisp rows, so callers must not treat a zero-commit as 'nothing happened'.","triggerScenarios":"Calling WakeExpiredDefers(ctx) when the internal SELECT of expired defers or the subsequent UPDATEs (issues and/or wisps tables) fail: driver errors, missing tables, scan errors, or transaction aborts.","commonSituations":"wisp tables absent on an older schema without optional-table probing succeeding, connection drop mid-sweep, lock contention with concurrent reclaim/wake jobs, or timeout with many expired defers.","solutions":["Read the wrapped cause; if wisp tables are missing, run migrations so optional tables exist.","Serialize wake/reclaim jobs (single background worker or lock) to avoid lock contention.","Retry the sweep on transient errors — it is safe to re-run.","Batch or tune timeouts if large defer backlogs cause slow sweeps."],"exampleFix":"// before\nissues, wisps, err := repo.WakeExpiredDefers(ctx)\nif err != nil { log.Fatal(err) }\n// after\nissues, wisps, err := repo.WakeExpiredDefers(ctx)\nif err != nil {\n    log.Printf(\"defer wake failed, will retry: %v\", err) // idempotent sweep\n    return\n}","handlingStrategy":"retry","validationCode":"// optional: skip wisp traversal on schemas without wisp tables\n// (the repo probes tables itself; just ensure migrations have run)\nif err := runMigrations(db); err != nil { return err }","typeGuard":null,"tryCatchPattern":"issues, wisps, err := repo.WakeExpiredDefers(ctx)\nif err != nil {\n    if isTransientDBErr(err) { scheduleRetry(err); return }\n    log.Printf(\"wake expired defers failed: %v\", err)\n    return\n}","preventionTips":["Run the sweep from a single scheduled job (avoid overlapping workers).","Remember a wisp-only wake still commits wisp writes — never roll back on zero counts.","Ensure migrations create optional wisp tables in environments that use wisps.","Retry on transient errors; the sweep is idempotent."],"tags":["database","defer","maintenance"],"backgroundTag":"db-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}