Skip to content

xlogrecovery: drain prefetch pins after the redo loop (fixes DROP TABLE spinning forever after crash recovery) - #94

Open
jackwangfeng wants to merge 1 commit into
malisper:mainfrom
jackwangfeng:fix/recovery-prefetch-pin-leak
Open

xlogrecovery: drain prefetch pins after the redo loop (fixes DROP TABLE spinning forever after crash recovery)#94
jackwangfeng wants to merge 1 commit into
malisper:mainfrom
jackwangfeng:fix/recovery-prefetch-pin-leak

Conversation

@jackwangfeng

@jackwangfeng jackwangfeng commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #93.

Problem

After a crash (kill -9) and successful WAL recovery, DROP TABLE on a relation whose pages were replayed by redo never returns: the transaction commits, then the backend thread spins at 100% CPU forever in bufmgr::drop_buffers::InvalidateBuffer, and pg_cancel_backend has no effect. Full analysis, stack traces and pg_buffercache evidence are in #93.

Root cause

The WAL prefetcher issues io_uring reads through PrefetchSharedBuffer -> uring::start_read. start_read pins the victim buffer and hands the pin to the issuing thread's ring slot (ForgetBufferPin); the pin is only dropped when that same thread runs collect_done (called at the start of the next start_read) or drain_own.

Redo issues no further prefetch after the last record, and nothing on the startup thread drains its ring at the end of recovery, so the pin on the last prefetched page is stranded. pg_buffercache shows exactly one buffer of the relation (its last block) with pinning_backends = 1 on an idle server after recovery.

InvalidateBuffer then sees shared refcount != 0, private refcount == 0 and no IO in progress, so WaitIO returns immediately and the continue loop never terminates.

Fix

Call bufmgr::uring_drain_pins() once the redo loop in perform_wal_recovery_guts ends. This is the same collect/drain discipline already used by AtEOXact_Buffers, LockBufferForCleanup and pool-worker ring teardown; the recovery path was the one place that issued prefetch reads without ever collecting them.

Verification (x86_64, Ubuntu 24.04, io_method=sync, default recovery_prefetch=try)

Scenario Before After
insert 100k rows, kill -9, restart, DROP TABLE spins forever 0.02-0.04 s (3/3 runs)
pg_buffercache pinned buffers of the relation after recovery, idle server 1 0
insert, CHECKPOINT, kill -9, restart, DROP ok ok
empty table, kill -9, restart, DROP ok ok
clean shutdown, restart, DROP ok ok
cargo test --release -p xlogrecovery -p bufmgr pass pass
basic SQL smoke test (DDL/DML/index/txn/JSONB/regex/CTE) ok ok

Setting recovery_prefetch=off also avoids the bug on the unpatched build, which independently confirms the prefetcher as the source.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved WAL recovery cleanup by ensuring in-flight prefetch operations are completed before recovery proceeds.
    • Helps prevent lingering resources after WAL replay finishes.

The WAL prefetcher issues io_uring reads whose buffer pins belong to the
startup thread's ring slots and are only dropped when that thread collects
them (bufmgr::uring::start_read calls collect_done on the next issue).
After the last redo record no further prefetch is issued, so the pin on
the last prefetched page was never collected.

Any later DropRelationsAllBuffers on that relation (DROP TABLE, TRUNCATE,
DROP DATABASE) then spins forever at 100% CPU in InvalidateBuffer: the
shared refcount is non-zero, the private refcount is zero, and no IO is
in progress, so WaitIO returns immediately and the loop retries. The
DROP has already committed at that point and pg_cancel_backend has no
effect.

Call bufmgr::uring_drain_pins() once the redo loop ends, matching the
collect/drain discipline used by AtEOXact_Buffers and pool-worker exit.

Repro: insert into a fresh table, kill -9 without a checkpoint, restart,
DROP TABLE. Verified with pg_buffercache that no buffer of the relation
stays pinned after recovery and that DROP completes in milliseconds.

Fixes malisper#93

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 6cd2095e-4de3-406e-b09f-4ccd76224cc8

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2e49f and 46068b8.

📒 Files selected for processing (1)
  • crates/backend/access/transam/xlogrecovery/src/lib.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

WAL recovery now drains pending io_uring reads after replay completes. It releases thread-owned buffer pins before recovery-target handling and subsequent cleanup.

Changes

WAL recovery cleanup

Layer / File(s) Summary
Post-replay buffer pin cleanup
crates/backend/access/transam/xlogrecovery/src/lib.rs
perform_wal_recovery_guts drains pending io_uring reads and releases their buffer pins after the final WAL record.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 46068

The change drains recovery-time prefetch pins after WAL replay so recovered relations can be invalidated normally; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the WAL recovery prefetch-pin fix and the resulting DROP TABLE hang after crash recovery.
Linked Issues check ✅ Passed The change drains io_uring-owned prefetch pins after the redo loop, directly addressing issue #93's leaked buffer pin and the resulting DROP TABLE infinite loop. The suggested InvalidateBuffer hardeni…
Out of Scope Changes check ✅ Passed The changes are limited to the WAL recovery cleanup path and directly support issue #93. No unrelated code changes are identified.
Full details: Linked Issues check

Explanation

The change drains io_uring-owned prefetch pins after the redo loop, directly addressing issue #93's leaked buffer pin and the resulting DROP TABLE infinite loop. The suggested InvalidateBuffer hardening is independent and is not required for the primary fix.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DROP TABLE spins forever at 100% CPU after crash recovery (leaked buffer pin from redo, uncancellable loop in InvalidateBuffer)

1 participant