gastownhall/beads · error
db: IssueSQLRepository.WakeExpiredDefers: %w
Error message
db: IssueSQLRepository.WakeExpiredDefers: %w
What it means
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'.
Source
Thrown at internal/storage/domain/db/issue.go:1263
return fmt.Errorf("db: IssueSQLRepository.HeartbeatIssue: %w: %s is ephemeral", storage.ErrNotClaimable, id)
}
if err := issueops.HeartbeatIssueInTx(ctx, r.runner, id, actor); err != nil {
return fmt.Errorf("db: IssueSQLRepository.HeartbeatIssue: %w", err)
}
return nil
}
// WakeExpiredDefers runs the shared lazy defer-wake body against this
// 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
- 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.
Example fix
// before
issues, wisps, err := repo.WakeExpiredDefers(ctx)
if err != nil { log.Fatal(err) }
// after
issues, wisps, err := repo.WakeExpiredDefers(ctx)
if err != nil {
log.Printf("defer wake failed, will retry: %v", err) // idempotent sweep
return
} Defensive patterns
Strategy: retry
Validate before calling
// optional: skip wisp traversal on schemas without wisp tables
// (the repo probes tables itself; just ensure migrations have run)
if err := runMigrations(db); err != nil { return err } Try / catch
issues, wisps, err := repo.WakeExpiredDefers(ctx)
if err != nil {
if isTransientDBErr(err) { scheduleRetry(err); return }
log.Printf("wake expired defers failed: %v", err)
return
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6d12a485a7771cec.
Report an issue: GitHub.