-- test/sql/pending_reclaim.sql -- -- Pending-list page reclaim. A merge (VACUUM -> amvacuumcleanup -> -- pg_tre_pending_merge) consumes the pending list into the posting/upper -- trees and then advances (or clears) meta.pending_head. Before this -- fix the consumed pages were simply abandoned -- the code said -- "orphaned until REINDEX" -- so an index under steady insert traffic -- leaked its entire pending list on every merge. -- -- A field report showed 716 pending pages surviving VACUUM (5176 kB vs -- 1600 kB for the same data freshly built), with the meta page already -- reporting pending_head = InvalidBlockNumber and pending_n_entries = 0: -- the entries had merged correctly, the pages were just never freed. -- -- The consumed pages now go to the deferred free log (the same XID-gated -- mechanism posting-leaf recycling uses), so they return to the FSM and -- get reused. -- -- This test verifies: -- 1. inserting into an EXISTING index populates the pending list -- (fastupdate is on by default, so aminsert appends there); -- 2. VACUUM merges it and leaves ZERO pending pages behind -- (pre-fix this stayed at its pre-VACUUM count forever); -- 3. the index still answers correctly after the merge (the pages -- being freed must be genuinely unreachable -- freeing a live page -- would corrupt results, which is far worse than the leak); -- 4. repeated insert/VACUUM cycles do not accumulate pending pages. CREATE EXTENSION IF NOT EXISTS pg_tre; psql:test/sql/pending_reclaim.sql:29: NOTICE: extension "pg_tre" already exists, skipping CREATE EXTENSION DROP TABLE IF EXISTS prc CASCADE; psql:test/sql/pending_reclaim.sql:31: NOTICE: table "prc" does not exist, skipping DROP TABLE CREATE TABLE prc (id serial PRIMARY KEY, body text); CREATE TABLE -- Build the index FIRST, then insert: that routes every entry through -- the pending list rather than the bulk build path. SET client_min_messages = warning; SET CREATE INDEX prc_tre ON prc USING tre (body); CREATE INDEX RESET client_min_messages; RESET INSERT INTO prc (body) VALUES ('needle'), ('needle'), ('needle'); INSERT 0 3 INSERT INTO prc (body) SELECT 'filler' || md5(g::text) FROM generate_series(1, 3000) g; INSERT 0 3000 -- (1) The pending list is populated. SELECT count(*) > 0 AS pending_populated FROM tre_page_kind_histogram('prc_tre') WHERE page_kind = 'pending'; pending_populated ------------------- t (1 row) -- Ground truth before any merge. SELECT count(*) AS rows_needle_before FROM prc WHERE body ~ 'needle'; rows_needle_before -------------------- 3 (1 row) -- (2) VACUUM merges the list and reclaims its pages. Two passes plus an -- XID bump: the free log is XID-gated (a page is only handed to the -- FSM once its deletion XID is globally removable), which is the same -- discipline nbtree uses for deleted pages. VACUUM prc; VACUUM SELECT txid_current() IS NOT NULL AS bumped_xid; bumped_xid ------------ t (1 row) VACUUM prc; VACUUM SELECT coalesce(sum(n_pages), 0) AS pending_pages_after_merge FROM tre_page_kind_histogram('prc_tre') WHERE page_kind = 'pending'; pending_pages_after_merge --------------------------- 0 (1 row) -- (3) Results must be unchanged: index vs sequential ground truth. If -- the reclaim ever frees a page still reachable from the tree, this -- is where it shows up. SET enable_seqscan = off; SET SELECT count(*) AS idx_needle FROM prc WHERE body ~ 'needle'; idx_needle ------------ 3 (1 row) SELECT count(*) AS idx_filler_one FROM prc WHERE body ~ ('filler' || md5('7')); idx_filler_one ---------------- 1 (1 row) SET enable_indexscan = off; SET SET enable_bitmapscan = off; SET SET enable_seqscan = on; SET SELECT count(*) AS seq_needle FROM prc WHERE body ~ 'needle'; seq_needle ------------ 3 (1 row) SELECT count(*) AS seq_filler_one FROM prc WHERE body ~ ('filler' || md5('7')); seq_filler_one ---------------- 1 (1 row) RESET enable_indexscan; RESET RESET enable_bitmapscan; RESET -- (4) Cycle again: pending pages must not accumulate across merges. INSERT INTO prc (body) SELECT 'second' || md5(g::text) FROM generate_series(1, 3000) g; INSERT 0 3000 VACUUM prc; VACUUM SELECT txid_current() IS NOT NULL AS bumped_xid_2; bumped_xid_2 -------------- t (1 row) VACUUM prc; VACUUM SELECT coalesce(sum(n_pages), 0) AS pending_pages_after_second_merge FROM tre_page_kind_histogram('prc_tre') WHERE page_kind = 'pending'; pending_pages_after_second_merge ---------------------------------- 0 (1 row) -- ...and the second batch is findable through the index. SET enable_seqscan = off; SET SELECT count(*) AS idx_second FROM prc WHERE body ~ ('second' || md5('42')); idx_second ------------ 1 (1 row) SET enable_indexscan = off; SET SET enable_bitmapscan = off; SET SET enable_seqscan = on; SET SELECT count(*) AS seq_second FROM prc WHERE body ~ ('second' || md5('42')); seq_second ------------ 1 (1 row) RESET enable_indexscan; RESET RESET enable_bitmapscan; RESET DROP TABLE prc CASCADE; DROP TABLE