Hi,

Masahiko Sawada <[email protected]> wrote (in August 2024):
> My question is;
> in order to track just catalog-change transactions, whether it's
> sufficient to check if XLOG_XACT_COMMIT[_PREPARED] has the
> XACT_XINFO_HAS_INVALS flag. If yes, we probably should change only
> xact_decode() to check the commit records even in BUILDING_SNAPSHOT.
> Otherwise, we would need to change mostly all paths where we mark the
> transaction as catalog-change as the patch does.

I tried to answer that with data, and it looks like the answer is no.

I added two LOG lines to master (the attached instrumentation diff, not
meant to be applied): SnapBuildCommitTxn() logs, for the xid and each
subxid, whether the reorder buffer has it as catalog-changing and
whether the commit has XACT_XINFO_HAS_INVALS; SnapBuildProcessNewCid()
logs which relation the NEW_CID is for.  Then I ran installcheck-parallel
with a logical slot created beforehand, and decoded all of it.  The
attached catchk_run.sh does all of this on a build with that diff,
including the user catalog table check below; three runs gave the same
counts.

    catalog changes   invals in commit   top-level xacts
          no                 no                5087
          yes                no                  37
          yes                yes              15540

The 37 are two kinds.  33 changed only pg_largeobject and
pg_largeobject_metadata.  In the other 4 the catalog changes were made
only in subtransactions that were rolled back (TRUNCATE and DROP TABLE
in stats.sql, and one pg_statistic write), so the top transaction was
marked through its child but had nothing left to invalidate.  In that
run, every committed change to a catalog with a syscache or relcache
had invalidations.

But the regression tests don't write to user catalog tables, and those
behave like large objects:

    CREATE TABLE uc (k int PRIMARY KEY, v text)
        WITH (user_catalog_table = true);

    INSERT INTO uc ...        catalog changes: yes   invals: no
    UPDATE uc ...             catalog changes: yes   invals: no
    DELETE FROM uc ...        catalog changes: yes   invals: no
    ALTER TABLE plain ...     catalog changes: yes   invals: yes

Output plugins may read user catalog tables with the historic snapshot,
so a transaction that writes one during BUILDING_SNAPSHOT has to be
tracked, and XACT_XINFO_HAS_INVALS would miss it.  So I think the
patch's approach, marking the transaction from the NEW_CID records as
well, is the one needed.

The same data says something about master that I can't turn into a
failure: SnapBuildXidHasCatalogChanges() relies on "The transactions
that have changed catalogs must have invalidation info" to skip the
catchange array lookup, which doesn't hold for user catalog tables or
large objects.  Is that a problem when a snapshot is restored, for a
plugin that reads user catalog tables?

On the patch itself, v6 applied on master e8a3ee5b197, against that
master, with --enable-cassert:

- snapshot_build fails on master with "could not map filenumber
  "base/16384/16772" to relation OID" and passes with v6.
- The same test with the catalog change in a subtransaction, and with
  it committed by COMMIT PREPARED, fails the same way on master and
  passes with v6.
- With ALTER TABLE ... ADD COLUMN on an existing table instead of
  CREATE TABLE, master raises no error: the insert that follows is
  decoded with the old tuple descriptor, and the new columns are
  silently dropped:

      master:  table public.tbl3: INSERT: val1[integer]:1
      v6:      table public.tbl3: INSERT: val1[integer]:1 val2[text]:'two' 
val3[bigint]:3

  That seems worse than the error, since nothing tells the user.

The attached diff, on top of v6-0002, adds these three as permutations
of snapshot_build.  They fail on master and pass with v6.

About Ajin's question in March 2025 on the DecodeTXNNeedSkip() change
(the "SnapBuildCurrentState(...) < SNAPBUILD_CONSISTENT" test), which
ChangAo said might be redundant: on v6 I logged every call made before
CONSISTENT, and whether SnapBuildXactNeedsSkip() already skipped it.
Over test_decoding, src/test/subscription and the recovery tests below
plus 040, it was called 41 times before CONSISTENT (28 in
BUILDING_SNAPSHOT, 13 in FULL_SNAPSHOT), and SnapBuildXactNeedsSkip()
had already skipped every one of them.  So it never changes the result
there.  Maybe an Assert would say the same thing and catch a case where
it does.

With v6: test_decoding (20 and 15, plus the new permutations),
src/test/subscription (605 tests), the logical decoding tests in
src/test/recovery (006, 035 and 038, 119 tests) and make check (239)
pass.

v6-0001 applies cleanly to master and REL_18_STABLE; REL_19_STABLE
conflicts in DecodeTXNNeedSkip(), and REL_17_STABLE and older in the
XLOG_XACT_INVALIDATIONS case of xact_decode().  As Haiyang reported in
bug #19109, it reproduces back to 11.

I haven't looked at Ajin's alternative beyond your question about it.

Regards,
Manu
diff --git a/contrib/test_decoding/expected/snapshot_build.out 
b/contrib/test_decoding/expected/snapshot_build.out
index 0fcf20cce86..32e5cd1dd1d 100644
--- a/contrib/test_decoding/expected/snapshot_build.out
+++ b/contrib/test_decoding/expected/snapshot_build.out
@@ -31,3 +31,100 @@ COMMIT
 stop    
 (1 row)
 
+
+starting permutation: s1_begin s1_insert s2_init s3_begin s3_insert 
s4_create_subxact s1_commit s4_begin s4_insert s3_commit s4_commit 
s2_get_changes
+step s1_begin: BEGIN;
+step s1_insert: INSERT INTO tbl1 VALUES (1);
+step s2_init: SELECT 'init' FROM 
pg_create_logical_replication_slot('isolation_slot', 'test_decoding'); <waiting 
...>
+step s3_begin: BEGIN;
+step s3_insert: INSERT INTO tbl1 VALUES (1);
+step s4_create_subxact: BEGIN; SAVEPOINT a; CREATE TABLE tbl2 (val1 integer); 
RELEASE a; COMMIT;
+step s1_commit: COMMIT;
+step s4_begin: BEGIN;
+step s4_insert: INSERT INTO tbl2 VALUES (1);
+step s3_commit: COMMIT;
+step s2_init: <... completed>
+?column?
+--------
+init    
+(1 row)
+
+step s4_commit: COMMIT;
+step s2_get_changes: SELECT data FROM 
pg_logical_slot_get_changes('isolation_slot', NULL, NULL, 'skip-empty-xacts', 
'1', 'include-xids', '0');
+data                                      
+------------------------------------------
+BEGIN                                     
+table public.tbl2: INSERT: val1[integer]:1
+COMMIT                                    
+(3 rows)
+
+?column?
+--------
+stop    
+(1 row)
+
+
+starting permutation: s1_begin s1_insert s2_init s3_begin s3_insert s4_prepare 
s4_commit_prepared s1_commit s4_begin s4_insert s3_commit s4_commit 
s2_get_changes
+step s1_begin: BEGIN;
+step s1_insert: INSERT INTO tbl1 VALUES (1);
+step s2_init: SELECT 'init' FROM 
pg_create_logical_replication_slot('isolation_slot', 'test_decoding'); <waiting 
...>
+step s3_begin: BEGIN;
+step s3_insert: INSERT INTO tbl1 VALUES (1);
+step s4_prepare: BEGIN; CREATE TABLE tbl2 (val1 integer); PREPARE TRANSACTION 
'snapshot_build';
+step s4_commit_prepared: COMMIT PREPARED 'snapshot_build';
+step s1_commit: COMMIT;
+step s4_begin: BEGIN;
+step s4_insert: INSERT INTO tbl2 VALUES (1);
+step s3_commit: COMMIT;
+step s2_init: <... completed>
+?column?
+--------
+init    
+(1 row)
+
+step s4_commit: COMMIT;
+step s2_get_changes: SELECT data FROM 
pg_logical_slot_get_changes('isolation_slot', NULL, NULL, 'skip-empty-xacts', 
'1', 'include-xids', '0');
+data                                      
+------------------------------------------
+BEGIN                                     
+table public.tbl2: INSERT: val1[integer]:1
+COMMIT                                    
+(3 rows)
+
+?column?
+--------
+stop    
+(1 row)
+
+
+starting permutation: s1_begin s1_insert s2_init s3_begin s3_insert s4_alter 
s1_commit s4_begin s4_insert3 s3_commit s4_commit s2_get_changes
+step s1_begin: BEGIN;
+step s1_insert: INSERT INTO tbl1 VALUES (1);
+step s2_init: SELECT 'init' FROM 
pg_create_logical_replication_slot('isolation_slot', 'test_decoding'); <waiting 
...>
+step s3_begin: BEGIN;
+step s3_insert: INSERT INTO tbl1 VALUES (1);
+step s4_alter: ALTER TABLE tbl3 ADD COLUMN val2 text, ADD COLUMN val3 bigint;
+step s1_commit: COMMIT;
+step s4_begin: BEGIN;
+step s4_insert3: INSERT INTO tbl3 VALUES (1, 'two', 3);
+step s3_commit: COMMIT;
+step s2_init: <... completed>
+?column?
+--------
+init    
+(1 row)
+
+step s4_commit: COMMIT;
+step s2_get_changes: SELECT data FROM 
pg_logical_slot_get_changes('isolation_slot', NULL, NULL, 'skip-empty-xacts', 
'1', 'include-xids', '0');
+data                                                                      
+--------------------------------------------------------------------------
+BEGIN                                                                     
+table public.tbl3: INSERT: val1[integer]:1 val2[text]:'two' val3[bigint]:3
+COMMIT                                                                    
+(3 rows)
+
+?column?
+--------
+stop    
+(1 row)
+
diff --git a/contrib/test_decoding/specs/snapshot_build.spec 
b/contrib/test_decoding/specs/snapshot_build.spec
index 334531dd219..65625e548ed 100644
--- a/contrib/test_decoding/specs/snapshot_build.spec
+++ b/contrib/test_decoding/specs/snapshot_build.spec
@@ -4,13 +4,16 @@ setup
 {
     DROP TABLE IF EXISTS tbl1;
     DROP TABLE IF EXISTS tbl2;
+    DROP TABLE IF EXISTS tbl3;
     CREATE TABLE tbl1 (val1 integer);
+    CREATE TABLE tbl3 (val1 integer);
 }
 
 teardown
 {
     DROP TABLE tbl1;
-    DROP TABLE tbl2;
+    DROP TABLE IF EXISTS tbl2;
+    DROP TABLE tbl3;
     SELECT 'stop' FROM pg_drop_replication_slot('isolation_slot');
 }
 
@@ -34,8 +37,13 @@ step "s3_commit" { COMMIT; }
 session "s4"
 setup { SET synchronous_commit=on; }
 step "s4_create" { CREATE TABLE tbl2 (val1 integer); }
+step "s4_create_subxact" { BEGIN; SAVEPOINT a; CREATE TABLE tbl2 (val1 
integer); RELEASE a; COMMIT; }
+step "s4_prepare" { BEGIN; CREATE TABLE tbl2 (val1 integer); PREPARE 
TRANSACTION 'snapshot_build'; }
+step "s4_commit_prepared" { COMMIT PREPARED 'snapshot_build'; }
+step "s4_alter" { ALTER TABLE tbl3 ADD COLUMN val2 text, ADD COLUMN val3 
bigint; }
 step "s4_begin" { BEGIN; }
 step "s4_insert" { INSERT INTO tbl2 VALUES (1); }
+step "s4_insert3" { INSERT INTO tbl3 VALUES (1, 'two', 3); }
 step "s4_commit" { COMMIT; }
 
 # T1: s1_begin -> s1_insert -> BUILDING_SNAPSHOT -> s1_commit -> FULL_SNAPSHOT
@@ -44,3 +52,13 @@ step "s4_commit" { COMMIT; }
 # T4: FULL_SNAPSHOT -> s4_begin -> s4_insert -> CONSISTENT -> s4_commit
 # The snapshot must track T3 or the replay of T4 will fail because its 
snapshot cannot see tbl2
 permutation "s1_begin" "s1_insert" "s2_init" "s3_begin" "s3_insert" 
"s4_create" "s1_commit" "s4_begin" "s4_insert" "s3_commit" "s4_commit" 
"s2_get_changes"
+
+# The same, with the catalog change made in a subtransaction.
+permutation "s1_begin" "s1_insert" "s2_init" "s3_begin" "s3_insert" 
"s4_create_subxact" "s1_commit" "s4_begin" "s4_insert" "s3_commit" "s4_commit" 
"s2_get_changes"
+
+# The same, with the catalog change committed by COMMIT PREPARED.
+permutation "s1_begin" "s1_insert" "s2_init" "s3_begin" "s3_insert" 
"s4_prepare" "s4_commit_prepared" "s1_commit" "s4_begin" "s4_insert" 
"s3_commit" "s4_commit" "s2_get_changes"
+
+# An ALTER TABLE instead: without tracking T3, the insert is decoded with the
+# old tuple descriptor and the new columns are silently dropped.
+permutation "s1_begin" "s1_insert" "s2_init" "s3_begin" "s3_insert" "s4_alter" 
"s1_commit" "s4_begin" "s4_insert3" "s3_commit" "s4_commit" "s2_get_changes"
diff --git a/src/backend/replication/logical/snapbuild.c 
b/src/backend/replication/logical/snapbuild.c
index de491ea0c4b..27ba27f4162 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -701,6 +701,10 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId 
xid,
         */
        ReorderBufferXidSetCatalogChanges(builder->reorder, xid, lsn);
 
+       /* EXPERIMENT: which catalog does this change touch? */
+       elog(LOG, "NEWCID xid=%u top=%u rel=%u",
+                xid, xlrec->top_xid, xlrec->target_locator.relNumber);
+
        ReorderBufferAddNewTupleCids(builder->reorder, xlrec->top_xid, lsn,
                                                                 
xlrec->target_locator, xlrec->target_tid,
                                                                 xlrec->cmin, 
xlrec->cmax,
@@ -968,6 +972,17 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, 
TransactionId xid,
                return;
        }
 
+       /* EXPERIMENT: does every catalog-changing xact carry invalidations? */
+       elog(LOG, "CATCHK state=%d xid=%u sub=0 rb=%d invals=%d",
+                builder->state, xid,
+                ReorderBufferXidHasCatalogChanges(builder->reorder, xid),
+                (xinfo & XACT_XINFO_HAS_INVALS) != 0);
+       for (int i = 0; i < nsubxacts; i++)
+               elog(LOG, "CATCHK state=%d xid=%u sub=1 rb=%d invals=%d",
+                        builder->state, subxacts[i],
+                        ReorderBufferXidHasCatalogChanges(builder->reorder, 
subxacts[i]),
+                        (xinfo & XACT_XINFO_HAS_INVALS) != 0);
+
        if (builder->state < SNAPBUILD_CONSISTENT)
        {
                /* ensure that only commits after this are getting replayed */
#!/bin/bash
# usage: catchk_run.sh <install prefix> <build dir> <port>
#
# Needs a build with nocfbot-catchk-instrumentation.diff applied.
#
# 1. Creates a logical slot, runs "make installcheck-parallel" from the build
#    dir against a throwaway cluster, decodes everything, and counts, per
#    committed xid, whether the reorder buffer had it as catalog-changing and
#    whether its commit had XACT_XINFO_HAS_INVALS.
# 2. For the xacts with catalog changes but no invals, lists the catalogs
#    their NEW_CID records were for, and whether the xids that touched them
#    committed (they appear as committed subxids) or were rolled back.
# 3. The same check for INSERT/UPDATE/DELETE on a user catalog table.
P=$1; B=$2; PORT=$3
D=/tmp/catchk_$PORT
rm -rf $D
$P/bin/initdb -D $D -U postgres --no-sync >/dev/null 2>&1
cat >> $D/postgresql.conf <<EOF
wal_level = logical
max_replication_slots = 4
max_wal_size = 8GB
EOF
$P/bin/pg_ctl -D $D -o "-p $PORT -k /tmp" -l $D/log -w start >/dev/null
trap '$P/bin/pg_ctl -D $D -m immediate -w stop >/dev/null 2>&1; rm -rf $D' EXIT
PSQL="$P/bin/psql -h /tmp -p $PORT -U postgres -X -q -At postgres"

$PSQL -c "SELECT 'slot' FROM pg_create_logical_replication_slot('catchk', 
'test_decoding')" >/dev/null
make -C $B/src/test/regress installcheck-parallel PGHOST=/tmp PGPORT=$PORT 
PGUSER=postgres \
  > $D/installcheck.log 2>&1
grep -E '# All|failed' $D/installcheck.log
$PSQL -c "SELECT count(*) FROM pg_logical_slot_get_changes('catchk', NULL, 
NULL)" >/dev/null

echo "== committed top-level xacts: catalog changes (rb) vs invals in commit"
grep -oE 'CATCHK state=2 xid=[0-9]+ sub=0 rb=[01] invals=[01]' $D/log |
  awk '{print $5, $6}' | sort | uniq -c

echo "== the ones with rb=1 invals=0, by catalog touched"
$PSQL -c "SELECT pg_relation_filenode(oid) || '|' || relname FROM pg_class
          WHERE relnamespace = 'pg_catalog'::regnamespace" > $D/filenodes
perl -e '
  open F, "$ARGV[0]"; while (<F>) { chomp; my ($n, $r) = split /\|/; $fn{$n} = 
$r }
  open L, "$ARGV[1]";
  while (<L>) {
    $bad{$1} = 1 if /CATCHK state=2 xid=(\d+) sub=0 rb=1 invals=0/;
    $committed{$1} = 1 if /CATCHK state=2 xid=(\d+) sub=1 /;
    push @{ $nc{$2} }, [$1, $3] if /NEWCID xid=(\d+) top=(\d+) rel=(\d+)/;
  }
  for my $x (keys %bad) {
    my (%cat, $aborted);
    for my $e (@{ $nc{$x} || [] }) {
      $cat{ $fn{$e->[1]} // "relfilenode $e->[1]" } = 1;
      $aborted = 1 if $e->[0] != $x && !$committed{ $e->[0] };
    }
    my $k = join(", ", sort keys %cat) . ($aborted ? "  (in rolled back 
subxacts)" : "");
    $g{$k}++;
  }
  printf "%5d  %s\n", $g{$_}, $_ for sort { $g{$b} <=> $g{$a} } keys %g;
' $D/filenodes $D/log

echo "== user catalog table"
$PSQL -c "CREATE TABLE uc (k int PRIMARY KEY, v text) WITH (user_catalog_table 
= true)" \
      -c "SELECT 'slot' FROM pg_create_logical_replication_slot('uc', 
'test_decoding')" >/dev/null
for q in "INSERT INTO uc VALUES (1, 'a')" "UPDATE uc SET v = 'b' WHERE k = 1" \
         "DELETE FROM uc WHERE k = 1" "ALTER TABLE uc ADD COLUMN w int"; do
  x=$($PSQL -c "BEGIN" -c "$q" -c "SELECT txid_current()" -c "COMMIT" | tail -1)
  echo "$x|$q"
done > $D/uc
$PSQL -c "SELECT count(*) FROM pg_logical_slot_get_changes('uc', NULL, NULL)" 
>/dev/null
while IFS='|' read -r x q; do
  printf "   %-36s %s\n" "$q" \
    "$(grep -oE "CATCHK state=2 xid=$x sub=0 rb=[01] invals=[01]" $D/log | tail 
-1 | grep -oE 'rb=[01] invals=[01]')"
done < $D/uc

Reply via email to