BUG #19794: Assert in btvacuumcleanup() fails when page counted in pages_free twice
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254370psql -h localhost -U postgresBuilt from patchset v2 (message #2), October 25, 2026 at 12:50 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t254370_2 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t254370_2 && git checkout t254370_2Patchset v2 (message #2) is on t254370_2
The following bug has been logged on the website:
Bug reference: 19794
Logged by: Alexander Lakhin
Email address: exclusion@gmail.com
PostgreSQL version: 19rc1
Operating system: Ubuntu 24.04
Description:
The following script:
echo "
CREATE TABLE t (id int);
CREATE INDEX t_i1 ON t (id);
CREATE INDEX t_i2 ON t (id);
INSERT INTO t SELECT g FROM generate_series(2000000, 1, -1) g;
DELETE FROM t WHERE id > 999000 AND id < 1000000;
VACUUM (INDEX_CLEANUP ON);
DELETE FROM t WHERE id > 1000000;
" | psql
echo "
SELECT 'SELECT txid_current();' FROM generate_series(1, 10000)
\gexec
" | psql >/dev/null &
psql -c 'VACUUM t'
wait
triggers:
TRAP: failed Assert("stats->pages_deleted >= stats->pages_free"), File:
"nbtree.c", Line: 1208, PID: 3843334
ExceptionalCondition at assert.c:51:13
btvacuumcleanup at nbtree.c:1209:22
index_vacuum_cleanup at indexam.c:698:1
vac_cleanup_one_index at vacuum.c:2743:10
parallel_vacuum_process_one_index at vacuumparallel.c:1171:16
parallel_vacuum_process_unsafe_indexes at vacuumparallel.c:1100:3
parallel_vacuum_process_all_indexes at vacuumparallel.c:981:2
parallel_vacuum_cleanup_all_indexes at vacuumparallel.c:636:1
lazy_cleanup_all_indexes at vacuumlazy.c:3019:2
lazy_scan_heap at vacuumlazy.c:1634:1
heap_vacuum_rel at vacuumlazy.c:878:31
table_relation_vacuum at tableam.h:1803:1
vacuum_rel at vacuum.c:2360:2
vacuum at vacuum.c:635:10
ExecVacuum at vacuum.c:470:2
standard_ProcessUtility at utility.c:861:4
ProcessUtility at utility.c:525:3
PortalRunUtility at pquery.c:1153:2
PortalRunMulti at pquery.c:1311:5
PortalRun at pquery.c:792:5
exec_simple_query at postgres.c:1304:11
PostgresMain at postgres.c:4950:27
BackendInitialize at backend_startup.c:142:1
postmaster_child_launch at launch_backend.c:269:3
BackendStartup at postmaster.c:3650:8
ServerLoop at postmaster.c:1730:10
PostmasterMain at postmaster.c:1414:11
main at main.c:232:2
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b)[0x7a3a2f82a28b]
postgres: postgres regression [local] VACUUM(_start+0x25)[0x5a37f1dd61b5]
2026-10-24 14:40:20.803 EEST [3843333:4] psql LOG: disconnection: session
time: 0:00:00.517 user=postgres database=regression host=[local]
2026-10-24 14:40:20.898 EEST [3843260:6] LOG: client backend (PID 3843334)
was terminated by signal 6: Aborted
With diagnostic log added in btvacuumcleanup(), I can see:
2026-10-24 14:33:48.916 EEST [3829584:7] psql LOG: !!!btvacuumcleanup|
stats->pages_deleted: 4927, stats->pages_free: 4928
2026-10-24 14:33:48.916 EEST [3829584:8] psql CONTEXT: while scanning
relation "public.t"
2026-10-24 14:33:48.916 EEST [3829584:9] psql STATEMENT: VACUUM t
TRAP: failed Assert("stats->pages_deleted >= stats->pages_free"), File:
"nbtree.c", Line: 1209, PID: 3829584
Reproduced starting from 0f0cfb494.
Hi Alexander,
On 24 Oct 2026, Alexander Lakhin wrote:
stats->pages_deleted: 4927, stats->pages_free: 4928
It looks like _bt_pagedel() can delete a page ahead of the physical
scan. If it becomes recyclable before the scan reaches it, both
btvacuumpage() and _bt_pendingfsm_finalize() can count it as free.
0f0cfb494 seems to expose this by making parallel VACUUM workers inherit
PROC_IN_VACUUM. Their xmin previously held back the recycling horizon.
The pending-FSM optimization itself dates to 9dd963ae2534.
Without assertions, pages_deleted - pages_free underflows and stores a
huge btm_last_cleanup_num_delpages in the metapage, prompting an extra
cleanup-only index scan. I don't see an unsafe page reuse here:
_bt_allocbuf() rechecks recyclability under the page lock.
PFA a small fix: only queue pages at or behind scanblkno, matching the
existing pages_deleted accounting. Pages ahead are left to the normal
scan. This may defer recycling some pages until the next VACUUM if they
are not yet recyclable when the scan visits them.
Thank you!
Best regards, Andrey Borodin.