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