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

  1. Retry with a fresh context/timeout; large-journal scans under contention are the usual cause.
  2. Ensure bd_events_journal exists via migrations.
  3. Run pruning more frequently so the journal stays small and the ceiling query cheap.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3ae07244b5faee8a. Report an issue: GitHub.