gastownhall/beads · error
journal: compute retain-rows floor: %w
Error message
journal: compute retain-rows floor: %w
What it means
In the retain-rows pruning strategy, ComputeEventsAutoPruneBoundInTx computes the ceiling seq (the oldest seq still retained when keeping `retainRows` newest rows) via EventsPruneRowsCeilQuery(). This error wraps a Scan failure of that aggregate query. ErrNoRows is handled separately (returns no bound), so this is a genuine driver error.
Source
Thrown at internal/storage/issueops/journal_prune.go:386
// eventsPruneFloorReadersInTx returns the two substrate reads
// ComputeEventsPruneWhere resolves its floors with, bound to one transaction.
// Both prune entry points — the explicit `bd events prune` below and the
// automatic bounding in journal_autoprune.go — take their readers from here, so
// a floor cannot mean one thing when an operator asks for it and another when
// maintenance applies it.
func eventsPruneFloorReadersInTx(ctx context.Context, tx DBTX, retainRows int) (
func() (int64, bool, error),
func(time.Time) (int64, bool, error),
) {
readRowsCeil := func() (int64, bool, error) {
var ceil int64
scanErr := tx.QueryRowContext(ctx, EventsPruneRowsCeilQuery(), retainRows).Scan(&ceil)
if errors.Is(scanErr, sql.ErrNoRows) {
return 0, false, nil
}
if scanErr != nil {
return 0, false, fmt.Errorf("journal: compute retain-rows floor: %w", scanErr)
}
return ceil, true, nil
}
readDaysFloor := func(cutoff time.Time) (int64, bool, error) {
// MIN over no matching rows yields one NULL row, not ErrNoRows.
var floorSeq sql.NullInt64
scanErr := tx.QueryRowContext(ctx, EventsPruneDaysFloorQuery(), cutoff).Scan(&floorSeq)
if errors.Is(scanErr, sql.ErrNoRows) {
return 0, false, nil
}
if scanErr != nil {
return 0, false, fmt.Errorf("journal: compute retain-days floor: %w", scanErr)
}
if !floorSeq.Valid {
return 0, false, nil
}
return floorSeq.Int64, true, nil
}View on GitHub (pinned to 71377f2769)
Solutions
- Retry with a fresh context/timeout; large-journal scans under contention are the usual cause.
- Ensure bd_events_journal exists via migrations.
- Run pruning more frequently so the journal stays small and the ceiling query cheap.
- Check server logs for the underlying SQL error (permissions, timeouts).
Example fix
// before ctx := context.Background() // unbounded; killed by server timeout on huge journal // after ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() bound, ok, err := store.ComputeEventsAutoPruneBoundInTx(ctx, tx, opts)
Defensive patterns
Strategy: try-catch
Validate before calling
var n int64
if err := db.QueryRow("SELECT COUNT(*) FROM bd_events_journal").Scan(&n); err != nil {
return fmt.Errorf("journal missing/unreadable: %w", err)
} Try / catch
bound, ok, err := store.ComputeEventsAutoPruneBoundInTx(ctx, tx, opts)
if err != nil && strings.Contains(err.Error(), "journal: compute retain-rows floor") {
if isTransient(err) { return recomputeWithTimeout(30 * time.Second) }
return err
} Prevention
- Use bounded contexts for prune-bound computation.
- Prune regularly so the ceiling query stays cheap.
- Ensure migrations have run before pruning.
- Avoid overlapping prune jobs.
When it happens
Trigger: Calling the prune-bound computation when EventsPruneRowsCeilQuery fails: bd_events_journal missing, connection lost, query plan/permission failure, or a driver error in the ORDER BY ... LIMIT 1 offset computation over a large journal.
Common situations: Very large journals making the ceiling query slow enough to hit lock/context timeouts; pre-migration DB; concurrent prune locking the table.
Related errors
- journal: compute retain-days floor: %w
- journal: prune below %d: %w
- journal: advance seq counter: %w
- journal: read seq counter: %w
- journal: read seq counter: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/3ae07244b5faee8a.
Report an issue: GitHub.