{"record":{"id":"3ae07244b5faee8a","repo":"gastownhall/beads","slug":"journal-compute-retain-rows-floor-w","errorCode":null,"errorMessage":"journal: compute retain-rows floor: %w","messagePattern":"journal: compute retain-rows floor: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/journal_prune.go","lineNumber":386,"sourceCode":"\n// eventsPruneFloorReadersInTx returns the two substrate reads\n// ComputeEventsPruneWhere resolves its floors with, bound to one transaction.\n// Both prune entry points — the explicit `bd events prune` below and the\n// automatic bounding in journal_autoprune.go — take their readers from here, so\n// a floor cannot mean one thing when an operator asks for it and another when\n// maintenance applies it.\nfunc eventsPruneFloorReadersInTx(ctx context.Context, tx DBTX, retainRows int) (\n\tfunc() (int64, bool, error),\n\tfunc(time.Time) (int64, bool, error),\n) {\n\treadRowsCeil := func() (int64, bool, error) {\n\t\tvar ceil int64\n\t\tscanErr := tx.QueryRowContext(ctx, EventsPruneRowsCeilQuery(), retainRows).Scan(&ceil)\n\t\tif errors.Is(scanErr, sql.ErrNoRows) {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tif scanErr != nil {\n\t\t\treturn 0, false, fmt.Errorf(\"journal: compute retain-rows floor: %w\", scanErr)\n\t\t}\n\t\treturn ceil, true, nil\n\t}\n\treadDaysFloor := func(cutoff time.Time) (int64, bool, error) {\n\t\t// MIN over no matching rows yields one NULL row, not ErrNoRows.\n\t\tvar floorSeq sql.NullInt64\n\t\tscanErr := tx.QueryRowContext(ctx, EventsPruneDaysFloorQuery(), cutoff).Scan(&floorSeq)\n\t\tif errors.Is(scanErr, sql.ErrNoRows) {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tif scanErr != nil {\n\t\t\treturn 0, false, fmt.Errorf(\"journal: compute retain-days floor: %w\", scanErr)\n\t\t}\n\t\tif !floorSeq.Valid {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\treturn floorSeq.Int64, true, nil\n\t}","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal_prune.go#L368-L404","documentation":"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.","triggerScenarios":"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.","commonSituations":"Very large journals making the ceiling query slow enough to hit lock/context timeouts; pre-migration DB; concurrent prune locking the table.","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)."],"exampleFix":"// before\nctx := context.Background() // unbounded; killed by server timeout on huge journal\n// after\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\nbound, ok, err := store.ComputeEventsAutoPruneBoundInTx(ctx, tx, opts)","handlingStrategy":"try-catch","validationCode":"var n int64\nif err := db.QueryRow(\"SELECT COUNT(*) FROM bd_events_journal\").Scan(&n); err != nil {\n    return fmt.Errorf(\"journal missing/unreadable: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"bound, ok, err := store.ComputeEventsAutoPruneBoundInTx(ctx, tx, opts)\nif err != nil && strings.Contains(err.Error(), \"journal: compute retain-rows floor\") {\n    if isTransient(err) { return recomputeWithTimeout(30 * time.Second) }\n    return err\n}","preventionTips":["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."],"tags":["storage","database","journal","pruning"],"backgroundTag":"prune-bound-computation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}