From c98e719ec5d914dfa4c010f67e021645e9e95186 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Tue, 27 Oct 2026 17:49:46 +0000 Subject: [PATCH v1] Fix stale relcache fields for TOAST indexes during REINDEX CONCURRENTLY REINDEX CONCURRENTLY of a TOAST table builds the new index alongside the old one, so the table has two indexes until the swap and inserts go into both. Once the reindexing backend marks the new index ready for inserts, a relcache invalidation message is sent to all backends. A backend writing to the TOAST table keeps its TOAST indexes open, and while processing the invalidation it treats the TOAST index as a system relation and skips reloading the new index's relcache fields, indisready among them (see RelationReloadIndexInfo()). Such a backend never inserts into the new index, so the chunks it writes go missing after the rebuild. This later shows up as the "missing chunk number N for toast value" error when the value is read. Fix this by re-reading the pg_index row for TOAST indexes too and filling in the relcache fields, just like for an index on a user table. Note that replacing IsSystemRelation() with IsCatalogRelation() also reloads the relcache fields for the TOAST indexes of the logical replication conflict log tables, which seems fine. Reindexing system catalogs concurrently isn't allowed anyway, and their TOAST indexes are unaffected because they are still catalog relations and keep skipping the lookup in RelationReloadIndexInfo(). The check in RelationReloadIndexInfo() was introduced in 8ec943856a4, and the issue was uncovered by 5dc92b844e68, which added REINDEX CONCURRENTLY. Reported-by: Saicharanreddy Chinthareddy Author: Bharath Rupireddy Discussion: https://postgr.es/m/<> Backpatch-through: 14 --- src/backend/access/common/toast_internals.c | 3 + src/backend/utils/cache/relcache.c | 7 +- src/test/modules/injection_points/Makefile | 1 + .../expected/reindex_concurrently_toast.out | 84 +++++++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/reindex_concurrently_toast.spec | 81 ++++++++++++++++++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/test/modules/injection_points/expected/reindex_concurrently_toast.out create mode 100644 src/test/modules/injection_points/specs/reindex_concurrently_toast.spec diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 2bdc0b7d3f2..a42ca7ed76c 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -24,6 +24,7 @@ #include "catalog/catalog.h" #include "miscadmin.h" #include "utils/fmgroids.h" +#include "utils/injection_point.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -226,6 +227,8 @@ toast_save_datum(Relation rel, Datum value, &toastidxs, &num_indexes); + INJECTION_POINT("toast-save-datum-indexes-opened", NULL); + /* * Get the data pointer and length, and compute va_rawsize and va_extinfo. * diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index d8f04a05309..2541bea7d18 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -2319,14 +2319,17 @@ RelationReloadIndexInfo(Relation relation) RelationInitPhysicalAddr(relation); /* - * For a non-system index, there are fields of the pg_index row that are + * For a non-catalog index, there are fields of the pg_index row that are * allowed to change, so re-read that row and update the relcache entry. * Most of the info derived from pg_index (such as support function lookup * info) cannot change, and indeed the whole point of this routine is to * update the relcache entry without clobbering that data; so wholesale * replacement is not appropriate. + * + * This includes the TOAST indexes of non-catalog tables, whose fields do + * change, for example during concurrent index builds. */ - if (!IsSystemRelation(relation)) + if (!IsCatalogRelation(relation)) { HeapTuple tuple; Form_pg_index index; diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 775ad37860d..8e2dfaa2384 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -22,6 +22,7 @@ WAIT_CLEANUP_TST = $(if $(filter win32,$(PORTNAME)),,wait_cleanup) ISOLATION = basic \ inplace \ reindex_concurrently_deferred \ + reindex_concurrently_toast \ repack \ repack_commit_race \ repack_decode \ diff --git a/src/test/modules/injection_points/expected/reindex_concurrently_toast.out b/src/test/modules/injection_points/expected/reindex_concurrently_toast.out new file mode 100644 index 00000000000..4ce089806b0 --- /dev/null +++ b/src/test/modules/injection_points/expected/reindex_concurrently_toast.out @@ -0,0 +1,84 @@ +Parsed test spec with 3 sessions + +starting permutation: lock reindex ins1 unlock await_ready wake_ins inserted wake_ready await_swap ins2 wake_swap reindexed read +injection_points_attach +----------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step lock: BEGIN; LOCK TABLE reind_toast IN ROW EXCLUSIVE MODE; +step reindex: REINDEX TABLE CONCURRENTLY reind_toast; +step ins1: INSERT INTO reind_toast VALUES (2, repeat('y', 10000)); +step unlock: COMMIT; +step await_ready: SELECT wait_for_point('reindex-conc-index-built'); +wait_for_point +-------------- + +(1 row) + +step wake_ins: + SELECT injection_points_detach('toast-save-datum-indexes-opened'); + SELECT injection_points_wakeup('toast-save-datum-indexes-opened'); + +injection_points_detach +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +step ins1: <... completed> +step inserted: +step wake_ready: + SELECT injection_points_detach('reindex-conc-index-built'); + SELECT injection_points_wakeup('reindex-conc-index-built'); + +injection_points_detach +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +step await_swap: SELECT wait_for_point('reindex-relation-concurrently-before-swap'); +wait_for_point +-------------- + +(1 row) + +step ins2: INSERT INTO reind_toast VALUES (3, repeat('z', 10000)); +step wake_swap: + SELECT injection_points_detach('reindex-relation-concurrently-before-swap'); + SELECT injection_points_wakeup('reindex-relation-concurrently-before-swap'); + +injection_points_detach +----------------------- + +(1 row) + +injection_points_wakeup +----------------------- + +(1 row) + +step reindex: <... completed> +step reindexed: +step read: SELECT id, length(val) FROM reind_toast ORDER BY id; +id|length +--+------ + 1| 10000 + 2| 10000 + 3| 10000 +(3 rows) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 60148f96fda..97e7a736282 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -30,6 +30,7 @@ injection_points_isolation = [ 'basic', 'inplace', 'reindex_concurrently_deferred', + 'reindex_concurrently_toast', 'repack', 'repack_commit_race', 'repack_decode', diff --git a/src/test/modules/injection_points/specs/reindex_concurrently_toast.spec b/src/test/modules/injection_points/specs/reindex_concurrently_toast.spec new file mode 100644 index 00000000000..ba34a81adef --- /dev/null +++ b/src/test/modules/injection_points/specs/reindex_concurrently_toast.spec @@ -0,0 +1,81 @@ +# REINDEX CONCURRENTLY of a TOAST index +# +# Verify that a backend writing to a TOAST table starts inserting into the +# new TOAST index once it is marked ready, so that no chunk is missing after +# the rebuild. +# +# An injection point fires after the TOAST indexes are opened. + +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE reind_toast (id int, val text); + ALTER TABLE reind_toast ALTER COLUMN val SET STORAGE EXTERNAL; + INSERT INTO reind_toast VALUES (1, repeat('x', 10000)); + CREATE FUNCTION wait_for_point(text) RETURNS void LANGUAGE plpgsql AS $$ + BEGIN + LOOP + PERFORM 1 FROM pg_stat_activity WHERE wait_event = $1; + EXIT WHEN FOUND; + PERFORM pg_sleep(.1); + END LOOP; + END $$; +} + +teardown +{ + DROP TABLE reind_toast; + DROP FUNCTION wait_for_point; + DROP EXTENSION injection_points; +} + +# The rebuild, stopped once the new index is ready for inserts and again +# once it has been validated, before the swap. +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('reindex-conc-index-built', 'wait'); + SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait'); +} +step reindex { REINDEX TABLE CONCURRENTLY reind_toast; } +step reindexed { } + +# The writer, stopped with the TOAST indexes open. +session s2 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('toast-save-datum-indexes-opened', 'wait'); +} +step ins1 { INSERT INTO reind_toast VALUES (2, repeat('y', 10000)); } +step inserted { } +# Written after the validation, so only a maintained index has it. +step ins2 { INSERT INTO reind_toast VALUES (3, repeat('z', 10000)); } +step read { SELECT id, length(val) FROM reind_toast ORDER BY id; } + +# Holds the rebuild back until the writer has the TOAST indexes open. +session s3 +step lock { BEGIN; LOCK TABLE reind_toast IN ROW EXCLUSIVE MODE; } +step unlock { COMMIT; } +# A session that has been woken can still be reported as waiting on the +# point it came from, so wait for the name of the next one instead. +step await_ready { SELECT wait_for_point('reindex-conc-index-built'); } +step wake_ins +{ + SELECT injection_points_detach('toast-save-datum-indexes-opened'); + SELECT injection_points_wakeup('toast-save-datum-indexes-opened'); +} +step wake_ready +{ + SELECT injection_points_detach('reindex-conc-index-built'); + SELECT injection_points_wakeup('reindex-conc-index-built'); +} +step await_swap { SELECT wait_for_point('reindex-relation-concurrently-before-swap'); } +step wake_swap +{ + SELECT injection_points_detach('reindex-relation-concurrently-before-swap'); + SELECT injection_points_wakeup('reindex-relation-concurrently-before-swap'); +} + +permutation lock reindex ins1 unlock await_ready wake_ins inserted wake_ready await_swap ins2 wake_swap reindexed read -- 2.47.3