gastownhall/beads · error
journal: prune below %d: %w
Error message
journal: prune below %d: %w
What it means
PruneEventsInTx executes DELETE FROM bd_events_journal WHERE <computed bound> to drop journal rows below the computed seq. This error wraps the DELETE failing, and includes the `before` seq bound in the message. The prune itself was correctly computed (skip was false); only the deletion failed.
Source
Thrown at internal/storage/issueops/journal_prune.go:422
}
return readRowsCeil, readDaysFloor
}
// PruneEventsInTx deletes journal rows with seq below before, honoring the
// retain-days and retain-rows floors (0 disables a floor), and returns the
// number of rows deleted. It runs inside the caller's transaction.
func PruneEventsInTx(ctx context.Context, tx DBTX, before int64, retainDays, retainRows int, now time.Time) (int64, error) {
readRowsCeil, readDaysFloor := eventsPruneFloorReadersInTx(ctx, tx, retainRows)
where, args, skip, err := ComputeEventsPruneWhere(before, retainDays, retainRows, now, readRowsCeil, readDaysFloor)
if err != nil {
return 0, err
}
if skip {
return 0, nil
}
res, err := tx.ExecContext(ctx, "DELETE FROM bd_events_journal WHERE "+where, args...)
if err != nil {
return 0, fmt.Errorf("journal: prune below %d: %w", before, err)
}
return res.RowsAffected()
}
View on GitHub (pinned to 71377f2769)
Solutions
- Retry the prune — lock contention with readers is the most common cause and is transient.
- Ensure the DB user has DELETE privilege and the connection is not read-only.
- Prune in smaller batches (lower retention steps) so each DELETE stays under timeout limits.
- Schedule pruning outside peak write windows to reduce lock waits.
Example fix
// before
defer store.PruneEventsInTx(ctx, tx, bound) // giant single DELETE, times out
// after
for bound > 0 { // prune incrementally
step := bound; if step > 10000 { step = 10000 }
if _, err := store.PruneEventsInTx(ctx, tx, step); err != nil { return err }
bound -= step
} Defensive patterns
Strategy: retry
Validate before calling
var ro int
_ = db.QueryRow("SELECT @@read_only").Scan(&ro)
if ro == 1 { return errors.New("database is read-only; pruning will fail") } Try / catch
n, err := store.PruneEventsInTx(ctx, tx, before)
if err != nil && strings.Contains(err.Error(), "journal: prune below") {
if isLockWaitOrTransient(err) { return retryPrune(before) }
return fmt.Errorf("prune to %d failed; check DELETE grants and FK/trigger config: %w", before, err)
} Prevention
- Grant DELETE on bd_events_journal to the app user.
- Prune in off-peak windows and in bounded batches.
- Avoid triggers/FKs on the journal table that block deletes.
- Set server lock-wait timeouts high enough for large deletes.
When it happens
Trigger: PruneEventsInTx failing at ExecContext: foreign-key or trigger on bd_events_journal rejecting deletes, lock contention with concurrent journal readers/writers, read-only connection, lost connection mid-DELETE, or a corrupted `where` clause from a malformed bound.
Common situations: Long-running DELETE timing out under lock contention on an active journal; running with a read-only DB user; server-enforced max execution time exceeded on very large deletes.
Related errors
- journal: compute retain-rows floor: %w
- journal: compute retain-days floor: %w
- delete: drop deps: %w
- delete: drop wisp deps: %w
- delete: drop labels: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e8af424a32252518.
Report an issue: GitHub.