gastownhall/beads · error
db: IssueSQLRepository.ReclaimExpiredLeases: %w
Error message
db: IssueSQLRepository.ReclaimExpiredLeases: %w
What it means
Wraps a failure from issueops.ReclaimExpiredLeasesInTx, which finds leases older than the cutoff (optionally filtered) and reclaims them for the given actor, returning the reclaimed lease list. The wrapper only adds context; SQL, scan, or validation failures inside the helper surface here.
Source
Thrown at internal/storage/domain/db/issue.go:1272
// repository's runner (the same DBTX-shaped seam ReclaimExpiredLeases uses)
// and reports how many rows woke per table. The issues count decides whether
// the transaction's owner mints a dolt commit; the wisps count decides
// whether it must still issue a plain SQL commit — wisp tables are
// dolt_ignored, so a wisp-only wake mints no version commit, but a caller
// that treats it as "nothing happened" rolls the wisp writes back.
func (r *issueSQLRepositoryImpl) WakeExpiredDefers(ctx context.Context) (issues, wisps int, err error) {
out, err := issueops.WakeExpiredDefersInTx(ctx, r.runner)
if err != nil {
return 0, 0, fmt.Errorf("db: IssueSQLRepository.WakeExpiredDefers: %w", err)
}
return len(out.Issues), len(out.Wisps), nil
}
func (r *issueSQLRepositoryImpl) ReclaimExpiredLeases(ctx context.Context, olderThan time.Duration, filter types.ReclaimFilter, actor string) ([]types.ReclaimedLease, error) {
cutoff := time.Now().UTC().Add(-olderThan)
out, err := issueops.ReclaimExpiredLeasesInTx(ctx, r.runner, cutoff, filter, actor)
if err != nil {
return nil, fmt.Errorf("db: IssueSQLRepository.ReclaimExpiredLeases: %w", err)
}
return out, nil
}
const deleteBatchSize = 200
View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause; validate the ReclaimFilter fields against the current types API.
- Run a single reclaimer instance (or use locking) to avoid contention.
- Retry transient driver errors; reclamation is idempotent for already-reclaimed leases.
- Confirm lease/issues tables exist with current schema (migrations/doctor).
Example fix
// before
out, err := repo.ReclaimExpiredLeases(ctx, olderThan, filter, actor)
if err != nil { panic(err) }
// after
out, err := repo.ReclaimExpiredLeases(ctx, olderThan, filter, actor)
if err != nil {
log.Printf("lease reclaim failed, retrying later: %v", err)
return
} Defensive patterns
Strategy: retry
Validate before calling
// sanity-check inputs before the call
if olderThan <= 0 { return errors.New("olderThan must be positive") }
if actor == "" { return errors.New("actor required") }
if err := validateReclaimFilter(filter); err != nil { return err } Try / catch
out, err := repo.ReclaimExpiredLeases(ctx, olderThan, filter, actor)
if err != nil {
if isTransientDBErr(err) { return retryReclaim(err) }
return fmt.Errorf("reclaim failed: %w", err)
} Prevention
- Run a single reclaimer instance (or leader-election) to avoid row contention.
- Validate ReclaimFilter against current types constants after upgrades.
- Use consistent UTC time handling for olderThan cutoffs.
- Retry idempotently — already-reclaimed leases are harmless.
When it happens
Trigger: Calling ReclaimExpiredLeases(ctx, olderThan, filter, actor) when the cutoff SELECT, the reclaim UPDATEs, or event writes fail: driver errors, invalid filter values producing bad SQL, missing tables, or actor/lease validation errors in the helper.
Common situations: Background reclaimer running against a dropped connection, invalid ReclaimFilter fields after an API change, clock skew producing odd cutoffs, or concurrent reclaimers contending on the same rows.
Related errors
- db: Update %s: clear lease: %w
- db: IssueSQLRepository.UnclaimIssue: %w
- db: IssueSQLRepository.HeartbeatIssue: %w
- rename lease row: %w
- ErrTransaction
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6b62d4e895bbe210.
Report an issue: GitHub.