The comment in GetSnapshotData() defines transactionXmin like this:

TransactionXmin: the oldest xmin of any snapshot in use in the current transaction (this is the same as MyProc->xmin).
However, we don't update TransactionXmin when we update MyProc->xmin in SnapshotResetXmin(). So TransactionXmin can be older than MyProc->xmin.

I browsed around to see if that might cause trouble, and found one such case: SubTransGetTopmostTransaction() uses TransactionXmin as the cut-off point so that it doesn't try to perform lookups of old XIDs that might've already been truncated away from pg_subtrans. When TransactionXmin is older than MyProc->xmin, then it might do that. The attached isolation test demonstrates that case and produces an error:

ERROR:  could not access status of transaction 17190290
DETAIL:  Could not open file "pg_subtrans/0106": No such file or directory.

A straightforward fix is to ensure that TransactionXmin is updated whenever MyProc->xmin is:

diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index a1a0c2adeb6..f59830abd21 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -883,7 +883,7 @@ SnapshotResetXmin(void)
                                                                                
pairingheap_first(&RegisteredSnapshots));

        if (TransactionIdPrecedes(MyProc->xmin, minSnapshot->xmin))
-               MyProc->xmin = minSnapshot->xmin;
+               MyProc->xmin = TransactionXmin = minSnapshot->xmin;
 }

 /*

Anyone see a reason not to do that?


There are a two other places where we set MyProc->xmin without updating TransactionXmin:

1. In ProcessStandbyHSFeedbackMessage(). AFAICS that's OK because walsender doesn't use TransactionXmin for anything.

2. In SnapBuildInitialSnapshot(). I suppose that's also OK because the TransactionXmin isn't used. I don't quite remember when that function might be called though.

In any case, I propose that we set TransactionXmin in all of those cases as well, so that TransactionXmin is always the equal to MyProc->xmin. Maybe even rename it to MyProcXmin to make that more clear.

--
Heikki Linnakangas
Neon (https://neon.tech)
#
# Session 3 starts with a cursor on table and fetches one row.
# Then it opens another cursor and closes the old one.
#
# This advances MyProc->xmin without resetting TransactionXmin.
#
# Then when fetching from the 3nd cursor, it hits a row
# with xmin that is a subxid. The subxid is greater than
# snapshot xmin, so it calls SubTransGetTopmostTransaction() on it
# Its parent is newer than TransactionXmin but older than
# Myproc->xmin. The pg_subtrans that has been truncated already,
# SubTransGetTopmostTransaction() fails to find it, and you get
# ERROR:  could not access status of transaction 16280360
#
#
# Sessions 1 and 2 set up the rows with right XIDs for session 3 to
# scan and hit that problem
#

setup
{
DROP TABLE IF EXISTS atbl, btbl;
CREATE TABLE mytbl (subx integer, val integer) WITH (autovacuum_enabled=false);
INSERT INTO mytbl SELECT g, g FROM generate_series(1, 1000) g;

CREATE OR REPLACE FUNCTION gen_subxids (n integer, m integer)
 RETURNS VOID
 LANGUAGE plpgsql
AS $$
BEGIN
  for i in 0..m loop
    perform gen_subxids(n);
  end loop;
END;
$$;

CREATE OR REPLACE FUNCTION gen_subxids (n integer)
 RETURNS VOID
 LANGUAGE plpgsql
AS $$
BEGIN
  IF n <= 0 THEN
    EXECUTE 'INSERT INTO mytbl VALUES (1, 2)';
  ELSE
    PERFORM gen_subxids(n - 1);
  END IF;
EXCEPTION /* generates a subxid */
  WHEN raise_exception THEN NULL;
END;
$$;
}

teardown
{
 DROP TABLE mytbl;
 DROP FUNCTION gen_subxids(integer);
 DROP FUNCTION gen_subxids(integer, integer);
}

session s1
step s1begin	{ BEGIN; }
step s1gen	{ SELECT gen_subxids(100, 1000); }
step s1c	{ COMMIT; }

step checkpoint	{ CHECKPOINT; }

session s2
step s2begin	{ BEGIN; select txid_current(); }
step s2gen	{ SELECT gen_subxids(100, 1000); }
step s2c	{ COMMIT; }

# Session 3
session s3
step s3begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
step s3read	{ SELECT count(*) FROM mytbl; }

step s3adeclare	{ DECLARE acur CURSOR FOR SELECT * FROM mytbl; }
step s3afetchone { FETCH FROM acur; }
step s3afetchall { FETCH ALL FROM acur; }
step s3aclose	{ CLOSE acur; }

step s3bdeclare	{ DECLARE bcur CURSOR FOR SELECT * FROM mytbl; }
step s3bfetchone { FETCH FROM bcur; }
step s3bfetchall { FETCH ALL FROM bcur; }

step s3c	{ COMMIT; }

permutation s3begin s3adeclare s3afetchone s1begin s1gen s2begin s1gen s2gen s1c checkpoint s3bdeclare s3bfetchone s3aclose checkpoint  s3bfetchall s3c s2c

Reply via email to