fix: reclaim spilled memtable chain pages - #389
Conversation
Fixes timescale#386. ## Problem When `tp_spill_finalize` unlinks the in-flight memtable chain, the old chain pages remain on the index main fork with no reclaim path. Under many spills the relation grows by O(N×K) instead of O(active chain). ## Fix - **`tp_memtable_mark_chain_dead`**: before `tp_spill_finalize`, WAL-stamp each spilled chain page (including fragment continuations) `DEAD` with `ReadNextFullTransactionId()` as `dead_fxid`. - **`tp_reclaim_dead_memtable_pages`**: in `tp_vacuumcleanup` under `LW_SHARED`, scan main-fork blocks after the metapage; when `dead_fxid` precedes the global horizon (`FullTransactionIdPrecedes` vs `FullTransactionIdFromAllowableAt(ReadNextFullTransactionId(), GetOldestNonRemovableTransactionId(...))`), call `RecordFreeIndexPage`. - **`tp_memtable_alloc_page`**: prefer `GetFreeIndexPage` + zeroed reuse over `ExtendBufferedRel` on bootstrap, extend, and fragment paths. - **`bm25_memtable_dead_pages()`** SRF for regression / replication checks. ## Testing - `make installcheck` (PostgreSQL 17) - `memtable_spill_dead`, `memtable_reclaim` (single-cycle reuse + five spill→VACUUM cycles with bounded main-fork growth) - `vacuum_extended.out` updated for FSM-affected L0 block numbers - `test/scripts/replication_memtable_dead_reclaim.sh` (physical replication)
|
Review by Copilot; posting via @tjgreen42's account. Two correctness concerns plus one design note. Build is clean and tests pass; comments are about robustness under crash and an orphan class that isn't actually covered. 1. Mark-then-finalize ordering is not crash-safe.
The problem is what happens after the global xmin advances past The window between last mark and finalize is small (sub-millisecond on a hot system) but it's real, and the post-crash → next-autovacuum window for the corruption to land is long (minutes to hours). Two ways to fix:
I'd do both — reverse the order so the failure mode is "leak, not corrupt", and add the reachability check to cover everything the eager stamp misses (including #2 below). Regression: a debug GUC that PANICs between 2. Lost-bootstrap-race orphans in
Fix: either stamp these DEAD before the early return, or rely on the reachability check from #1 — it handles this class for free. 3. Design note. An alternative shape worth considering is to skip the spill-side DEAD stamping entirely and let vacuum do the work: walk reachability from Otherwise the shape is good: FSM integration is correct, the SRF + replication test are useful, and |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Hey @mory91, thanks much for getting this fix going! I'm pushing to cut a release by end of this week and would like to include this fix. If you've got time tonight/tomorrow morning to address the CI failures and PR feedback, that'd be great. Otherwise I will likely take over tomorrow afternoon to get this thing over the finish line in time. |
- Finalize before marking dead to prevent FSM corruption on crash - Add reachability check in vacuum to skip live-chain pages - Mark lost-bootstrap-race orphans DEAD for proper reclaim
7f86293 to
d670d45
Compare
|
Thanks for the thorough review. Both issues are now addressed: 1. Mark-then-finalize orderingImplemented both suggested fixes: 2. Lost-bootstrap-race orphansFixed by calling |
7b49d84 to
4cdd11f
Compare
4cdd11f to
80a0753
Compare
Document the correct finalize-first, mark-dead-second ordering and explain why the reverse would be unsafe. Also document the reachability check in vacuum as defense-in-depth.
Resolve conflicts with in-memory memtable cache feature (timescale#392): - build.c: keep crash-safe spill ordering, update tp_spill_finalize signature - log.h: merge crash-safe ordering docs with cache context - Makefile: combine test lists (cache_* + memtable_spill_dead/reclaim) - SQL files: add bm25_memtable_dead_pages to internal function list - CLAUDE.md: merge GUC tables
Apply clang-format 21.1.8 so the format-check CI job passes.
Release v1.3.0. ## Headlines - **On-disk memtable** (#374, #375, #385, #389): the L0 memtable now lives in the index relation itself as a chain of WAL-logged pages via `GenericXLog`, replacing the shared-memory structure. Removes the old soft-limit machinery and a long tail of physical-replication edge cases. New metapage version (v7), read-compatible with v6 via lazy upgrade. See [`docs/memtable_v2.md`](docs/memtable_v2.md). - **In-memory memtable cache** (#391, #392, #395): query reads can be served from a per-backend cache built over the on-disk chain, with a 3-tier memory cap (per-index / global soft / global hard). Controlled by `pg_textsearch.memtable_cache_enabled` and `pg_textsearch.memory_limit`. See [`docs/memtable_cache.md`](docs/memtable_cache.md). - **Multi-backend reindex regression coverage** (#386, #390, #396): fixes a stale-CTIDs class of bug that surfaced under ALTER TABLE heap rewrites concurrent with memtable activity. - **CI hardening** (#372, #394). ## Release checklist (from RELEASING.md) - [x] Audit `sql/pg_textsearch--1.2.0--1.3.0.sql` against the main SQL diff. Covers 11 new CREATE FUNCTIONs (memtable + cache test scaffolds), DROP of `bm25_memory_usage()`, and the two ALTER FUNCTION ... PARALLEL UNSAFE changes (`bm25_text_bm25query_score`, `bm25_textarray_bm25query_score`). - [x] Ran `./scripts/bump-version.sh 1.3.0-dev 1.3.0`. - [x] Replaced banner image (`images/tapir_and_friends_v1.3.0.png`). - [x] `1.2.0` is already present in the upgrade-tests matrix (added during the dev-bump in #373).
Fixes #379.
Problem
When
tp_spill_finalizeunlinks the in-flight memtable chain, the old chain pages remain on the index main fork with no reclaim path. Under many spills the relation grows by O(N×K) instead of O(active chain).Fix
tp_memtable_mark_chain_dead: beforetp_spill_finalize, WAL-stamp each spilled chain page (including fragment continuations)DEADwithReadNextFullTransactionId()asdead_fxid.tp_reclaim_dead_memtable_pages: intp_vacuumcleanupunderLW_SHARED, scan main-fork blocks after the metapage; whendead_fxidprecedes the global horizon (FullTransactionIdPrecedesvsFullTransactionIdFromAllowableAt(ReadNextFullTransactionId(), GetOldestNonRemovableTransactionId(...))), callRecordFreeIndexPage.tp_memtable_alloc_page: preferGetFreeIndexPage+ zeroed reuse overExtendBufferedRelon bootstrap, extend, and fragment paths.bm25_memtable_dead_pages()SRF for regression / replication checks.Testing
make installcheckmemtable_spill_dead,memtable_reclaim(single-cycle reuse + five spill→VACUUM cycles with bounded main-fork growth)vacuum_extended.outupdated for FSM-affected L0 block numberstest/scripts/replication_memtable_dead_reclaim.sh(physical replication)