agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v11 5/6] Move removal of old serialized snapshots to custodian.
26+ messages / 5 participants
[nested] [flat]

* [PATCH v10 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 7a710e6490..cbe86c6822 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6848,10 +6848,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1d8ebb4c0d..d3bbc59389 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2027,14 +2027,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index e6adea24f2..e1de013ece 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--+QahgC5+KEYLbs62
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v10-0006-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v20 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 contrib/test_decoding/expected/rewrite.out  | 21 +++++++++++++++++++++
 contrib/test_decoding/sql/rewrite.sql       | 17 +++++++++++++++++
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          |  2 ++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  2 +-
 src/include/replication/snapbuild.h         |  2 +-
 7 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/contrib/test_decoding/expected/rewrite.out b/contrib/test_decoding/expected/rewrite.out
index b30999c436..8b97f15f6f 100644
--- a/contrib/test_decoding/expected/rewrite.out
+++ b/contrib/test_decoding/expected/rewrite.out
@@ -162,3 +162,24 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+ ?column? 
+----------
+ t
+(1 row)
+
diff --git a/contrib/test_decoding/sql/rewrite.sql b/contrib/test_decoding/sql/rewrite.sql
index 62dead3a9b..d268fa559a 100644
--- a/contrib/test_decoding/sql/rewrite.sql
+++ b/contrib/test_decoding/sql/rewrite.sql
@@ -105,3 +105,20 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f9f0f6db8d..7da9461048 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6994,10 +6994,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index 98bb9efcfd..4e0ce1f7b3 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 62542827e4..f940bb5930 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2036,14 +2036,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index f49b941b53..5f1ba3842c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--CE+1k2dSO48ffgeK
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v20-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v6 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          | 12 ++++++++++++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  3 ++-
 src/include/replication/snapbuild.h         |  2 +-
 5 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8764084e21..621bda0844 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,13 +75,13 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/basebackup.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6840,10 +6840,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index a0ec94ea5c..861de882c6 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -31,6 +31,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -210,6 +211,17 @@ CustodianMain(void)
 		if (flags & CUSTODIAN_REMOVE_TEMP_FILES)
 			RemovePgTempFiles(false, false);
 
+		/*
+		 * Remove serialized snapshots that are no longer required by any
+		 * logical replication slot.
+		 *
+		 * It is not important for these to be removed in single-user mode, so
+		 * we don't need any extra handling outside of the custodian process for
+		 * this.
+		 */
+		if (flags & CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS)
+			RemoveOldSerializedSnapshots();
+
 		/* Calculate how long to sleep */
 		end_time = (pg_time_t) time(NULL);
 		elapsed_secs = end_time - start_time;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1119a12db9..42eb064bd8 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -1911,14 +1911,13 @@ snapshot_not_interesting:
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index f6dcd9ddef..769c07f2c9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,6 +18,7 @@ extern void CustodianShmemInit(void);
 extern void RequestCustodian(int flags);
 
 /* flags for RequestCustodian() */
-#define CUSTODIAN_REMOVE_TEMP_FILES		0x0001
+#define CUSTODIAN_REMOVE_TEMP_FILES				0x0001
+#define CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS	0x0002
 
 #endif						/* _CUSTODIAN_H */
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index d179251aad..55a2beb434 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--liOOAslEiF7prFVr
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v6-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v8 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9cedd6876f..72645f1fe6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6846,10 +6846,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1ff2c12240..abafdb52b2 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2014,14 +2014,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index e6adea24f2..e1de013ece 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--X1bOJ3K7DJ5YkBrT
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v8-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v16 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 contrib/test_decoding/expected/spill.out    | 21 +++++++++++++++++++++
 contrib/test_decoding/sql/spill.sql         | 17 +++++++++++++++++
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          |  2 ++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  2 +-
 src/include/replication/snapbuild.h         |  2 +-
 7 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/contrib/test_decoding/expected/spill.out b/contrib/test_decoding/expected/spill.out
index 10734bdb6a..75acbd5d5c 100644
--- a/contrib/test_decoding/expected/spill.out
+++ b/contrib/test_decoding/expected/spill.out
@@ -248,6 +248,27 @@ GROUP BY 1 ORDER BY 1;
 (2 rows)
 
 DROP TABLE spill_test;
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+ ?column? 
+----------
+ t
+(1 row)
+
 SELECT pg_drop_replication_slot('regression_slot');
  pg_drop_replication_slot 
 --------------------------
diff --git a/contrib/test_decoding/sql/spill.sql b/contrib/test_decoding/sql/spill.sql
index e638cacd3f..94d522f548 100644
--- a/contrib/test_decoding/sql/spill.sql
+++ b/contrib/test_decoding/sql/spill.sql
@@ -176,4 +176,21 @@ GROUP BY 1 ORDER BY 1;
 
 DROP TABLE spill_test;
 
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+
 SELECT pg_drop_replication_slot('regression_slot');
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..c153c32a77 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index a94381bc21..d0fd955d4b 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index beddcbcdea..e7c4f69b42 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2036,14 +2036,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--lrZ03NoBR/3+SXJZ
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v16-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v11 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index f32b2124e6..677cead44e 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7028,10 +7028,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1d8ebb4c0d..d3bbc59389 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2027,14 +2027,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index f126ff2e08..4877afb1bd 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--envbJBWh7q8WU6mo
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v11-0006-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v5 5/8] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           |  2 --
 src/backend/postmaster/custodian.c          | 11 +++++++++++
 src/backend/replication/logical/snapbuild.c | 13 +++++++------
 src/include/replication/snapbuild.h         |  2 +-
 4 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index ce78ac413e..c4a80ea82a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -79,7 +79,6 @@
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6807,7 +6806,6 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index 5bad0af474..8591c5db9b 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -40,6 +40,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -208,6 +209,16 @@ CustodianMain(void)
 		 */
 		RemovePgTempFiles(false, false);
 
+		/*
+		 * Remove serialized snapshots that are no longer required by any
+		 * logical replication slot.
+		 *
+		 * It is not important for these to be removed in single-user mode, so
+		 * we don't need any extra handling outside of the custodian process for
+		 * this.
+		 */
+		RemoveOldSerializedSnapshots();
+
 		/* Calculate how long to sleep */
 		end_time = (pg_time_t) time(NULL);
 		elapsed_secs = end_time - start_time;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 83fca8a77d..466a6478f3 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -125,6 +125,7 @@
 #include "access/xact.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logical.h"
 #include "replication/reorderbuffer.h"
 #include "replication/snapbuild.h"
@@ -1912,14 +1913,13 @@ snapshot_not_interesting:
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
@@ -1942,7 +1942,8 @@ CheckPointSnapBuild(void)
 		cutoff = redo;
 
 	snap_dir = AllocateDir("pg_logical/snapshots");
-	while ((snap_de = ReadDir(snap_dir, "pg_logical/snapshots")) != NULL)
+	while (!ShutdownRequestPending &&
+		   (snap_de = ReadDir(snap_dir, "pg_logical/snapshots")) != NULL)
 	{
 		uint32		hi;
 		uint32		lo;
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index d179251aad..55a2beb434 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--2oS5YaxWCcQjTEyO
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v5-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v19 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 contrib/test_decoding/expected/rewrite.out  | 21 +++++++++++++++++++++
 contrib/test_decoding/sql/rewrite.sql       | 17 +++++++++++++++++
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          |  2 ++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  2 +-
 src/include/replication/snapbuild.h         |  2 +-
 7 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/contrib/test_decoding/expected/rewrite.out b/contrib/test_decoding/expected/rewrite.out
index b30999c436..8b97f15f6f 100644
--- a/contrib/test_decoding/expected/rewrite.out
+++ b/contrib/test_decoding/expected/rewrite.out
@@ -162,3 +162,24 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+ ?column? 
+----------
+ t
+(1 row)
+
diff --git a/contrib/test_decoding/sql/rewrite.sql b/contrib/test_decoding/sql/rewrite.sql
index 62dead3a9b..d268fa559a 100644
--- a/contrib/test_decoding/sql/rewrite.sql
+++ b/contrib/test_decoding/sql/rewrite.sql
@@ -105,3 +105,20 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fb4c860bde..382e59f723 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6997,10 +6997,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index 98bb9efcfd..4e0ce1f7b3 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 829c568112..6b403a2bb4 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2036,14 +2036,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index f49b941b53..5f1ba3842c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--x+6KMIRAuhnl3hBn
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v19-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v9 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 87b243e0d4..88d10874e2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6842,10 +6842,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1ff2c12240..abafdb52b2 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2014,14 +2014,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index e6adea24f2..e1de013ece 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--y0ulUmNC+osPPQO6
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v9-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v14 2/3] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 6 ++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 2 +-
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..c153c32a77 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index a94381bc21..d0fd955d4b 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index a1fd1d92d6..f957b9aa49 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2037,14 +2037,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--BXVAT5kNtrzKuDFl
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v14-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v4 5/8] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           |  2 --
 src/backend/postmaster/custodian.c          | 11 +++++++++++
 src/backend/replication/logical/snapbuild.c | 13 +++++++------
 src/include/replication/snapbuild.h         |  2 +-
 4 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 958220c495..369e0711f1 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -56,7 +56,6 @@
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -9569,7 +9568,6 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index 79bc4a7065..0f4dbdd669 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -40,6 +40,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/proc.h"
@@ -207,6 +208,16 @@ CustodianMain(void)
 		 */
 		RemovePgTempFiles(false, false);
 
+		/*
+		 * Remove serialized snapshots that are no longer required by any
+		 * logical replication slot.
+		 *
+		 * It is not important for these to be removed in single-user mode, so
+		 * we don't need any extra handling outside of the custodian process for
+		 * this.
+		 */
+		RemoveOldSerializedSnapshots();
+
 		/* Calculate how long to sleep */
 		end_time = (pg_time_t) time(NULL);
 		elapsed_secs = end_time - start_time;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 83fca8a77d..466a6478f3 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -125,6 +125,7 @@
 #include "access/xact.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/interrupt.h"
 #include "replication/logical.h"
 #include "replication/reorderbuffer.h"
 #include "replication/snapbuild.h"
@@ -1912,14 +1913,13 @@ snapshot_not_interesting:
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
@@ -1942,7 +1942,8 @@ CheckPointSnapBuild(void)
 		cutoff = redo;
 
 	snap_dir = AllocateDir("pg_logical/snapshots");
-	while ((snap_de = ReadDir(snap_dir, "pg_logical/snapshots")) != NULL)
+	while (!ShutdownRequestPending &&
+		   (snap_de = ReadDir(snap_dir, "pg_logical/snapshots")) != NULL)
 	{
 		uint32		hi;
 		uint32		lo;
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index d179251aad..55a2beb434 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--BXVAT5kNtrzKuDFl
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v18 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 contrib/test_decoding/expected/rewrite.out  | 21 +++++++++++++++++++++
 contrib/test_decoding/sql/rewrite.sql       | 17 +++++++++++++++++
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          |  2 ++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  2 +-
 src/include/replication/snapbuild.h         |  2 +-
 7 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/contrib/test_decoding/expected/rewrite.out b/contrib/test_decoding/expected/rewrite.out
index b30999c436..8b97f15f6f 100644
--- a/contrib/test_decoding/expected/rewrite.out
+++ b/contrib/test_decoding/expected/rewrite.out
@@ -162,3 +162,24 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+ ?column? 
+----------
+ t
+(1 row)
+
diff --git a/contrib/test_decoding/sql/rewrite.sql b/contrib/test_decoding/sql/rewrite.sql
index 62dead3a9b..d268fa559a 100644
--- a/contrib/test_decoding/sql/rewrite.sql
+++ b/contrib/test_decoding/sql/rewrite.sql
@@ -105,3 +105,20 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..c153c32a77 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index e5af958999..9382d524a6 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index beddcbcdea..e7c4f69b42 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2036,14 +2036,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--LZvS9be/3tNcYl/X
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v18-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v10 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 7a710e6490..cbe86c6822 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6848,10 +6848,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1d8ebb4c0d..d3bbc59389 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2027,14 +2027,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index e6adea24f2..e1de013ece 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--+QahgC5+KEYLbs62
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v10-0006-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v12 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index be54c23187..03fdcf2c07 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7024,10 +7024,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 5006a5c464..a161cf3995 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2030,14 +2030,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--UlVJffcvxoiEqYs2
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v12-0006-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v13 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..4991c10f86 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index a1fd1d92d6..f957b9aa49 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2037,14 +2037,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--CE+1k2dSO48ffgeK
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v13-0006-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v7 5/6] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 8 ++++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 1 +
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1b2f240228..f08a18d273 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -75,13 +75,13 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/basebackup.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -6837,10 +6837,14 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
+					 !IsUnderPostmaster,
+					 (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index fe1f48844e..855a756ca0 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -71,6 +72,7 @@ struct cust_task_funcs_entry
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
 	{CUSTODIAN_REMOVE_TEMP_FILES, RemovePgTempFiles, NULL},
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 73c0f15214..b945744e9c 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -1911,14 +1911,13 @@ snapshot_not_interesting:
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 80890ceadd..37334941cc 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -19,6 +19,7 @@
 typedef enum CustodianTask
 {
 	CUSTODIAN_REMOVE_TEMP_FILES,
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index d179251aad..55a2beb434 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *cache,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--/04w6evG8XlLl3ft
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v7-0006-Move-removal-of-old-logical-rewrite-mapping-files.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v15 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 src/backend/access/transam/xlog.c           | 6 ++++--
 src/backend/postmaster/custodian.c          | 2 ++
 src/backend/replication/logical/snapbuild.c | 9 ++++-----
 src/include/postmaster/custodian.h          | 2 +-
 src/include/replication/snapbuild.h         | 2 +-
 5 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..c153c32a77 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index a94381bc21..d0fd955d4b 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index a1fd1d92d6..f957b9aa49 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2037,14 +2037,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--HcAYCG3uE/tztfnV
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v15-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* [PATCH v17 2/4] Move removal of old serialized snapshots to custodian.
@ 2021-12-06 06:02 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Nathan Bossart @ 2021-12-06 06:02 UTC (permalink / raw)

This was only done during checkpoints because it was a convenient
place to put it.  However, if there are many snapshots to remove,
it can significantly extend checkpoint time.  To avoid this, move
this work to the newly-introduced custodian process.
---
 contrib/test_decoding/expected/rewrite.out  | 21 +++++++++++++++++++++
 contrib/test_decoding/sql/rewrite.sql       | 17 +++++++++++++++++
 src/backend/access/transam/xlog.c           |  6 ++++--
 src/backend/postmaster/custodian.c          |  2 ++
 src/backend/replication/logical/snapbuild.c |  9 ++++-----
 src/include/postmaster/custodian.h          |  2 +-
 src/include/replication/snapbuild.h         |  2 +-
 7 files changed, 50 insertions(+), 9 deletions(-)

diff --git a/contrib/test_decoding/expected/rewrite.out b/contrib/test_decoding/expected/rewrite.out
index b30999c436..8b97f15f6f 100644
--- a/contrib/test_decoding/expected/rewrite.out
+++ b/contrib/test_decoding/expected/rewrite.out
@@ -162,3 +162,24 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
+ ?column? 
+----------
+ t
+(1 row)
+
diff --git a/contrib/test_decoding/sql/rewrite.sql b/contrib/test_decoding/sql/rewrite.sql
index 62dead3a9b..d268fa559a 100644
--- a/contrib/test_decoding/sql/rewrite.sql
+++ b/contrib/test_decoding/sql/rewrite.sql
@@ -105,3 +105,20 @@ DROP TABLE IF EXISTS replication_example;
 DROP FUNCTION iamalongfunction();
 DROP FUNCTION exec(text);
 DROP ROLE regress_justforcomments;
+
+-- make sure custodian cleans up files
+CHECKPOINT;
+DO $$
+DECLARE
+    snaps_removed bool;
+    loops int := 0;
+BEGIN
+    LOOP
+        snaps_removed := count(*) = 0 FROM pg_ls_logicalsnapdir();
+        IF snaps_removed OR loops > 120 * 100 THEN EXIT; END IF;
+        PERFORM pg_sleep(0.01);
+        loops := loops + 1;
+    END LOOP;
+END
+$$;
+SELECT count(*) = 0 FROM pg_ls_logicalsnapdir();
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a31fbbff78..c153c32a77 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -76,12 +76,12 @@
 #include "port/atomics.h"
 #include "port/pg_iovec.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/custodian.h"
 #include "postmaster/startup.h"
 #include "postmaster/walwriter.h"
 #include "replication/logical.h"
 #include "replication/origin.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
@@ -7001,10 +7001,12 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
 {
 	CheckPointRelationMap();
 	CheckPointReplicationSlots();
-	CheckPointSnapBuild();
 	CheckPointLogicalRewriteHeap();
 	CheckPointReplicationOrigin();
 
+	/* tasks offloaded to custodian */
+	RequestCustodian(CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, (Datum) 0);
+
 	/* Write out all dirty data in SLRUs and the main buffer pool */
 	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
 	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
diff --git a/src/backend/postmaster/custodian.c b/src/backend/postmaster/custodian.c
index a94381bc21..d0fd955d4b 100644
--- a/src/backend/postmaster/custodian.c
+++ b/src/backend/postmaster/custodian.c
@@ -25,6 +25,7 @@
 #include "pgstat.h"
 #include "postmaster/custodian.h"
 #include "postmaster/interrupt.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -70,6 +71,7 @@ struct cust_task_funcs_entry
  * whether the task is already enqueued.
  */
 static const struct cust_task_funcs_entry cust_task_functions[] = {
+	{CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS, RemoveOldSerializedSnapshots, NULL},
 	{INVALID_CUSTODIAN_TASK, NULL, NULL}	/* must be last */
 };
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index beddcbcdea..e7c4f69b42 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -2036,14 +2036,13 @@ SnapBuildRestoreContents(int fd, char *dest, Size size, const char *path)
 
 /*
  * Remove all serialized snapshots that are not required anymore because no
- * slot can need them. This doesn't actually have to run during a checkpoint,
- * but it's a convenient point to schedule this.
+ * slot can need them.
  *
- * NB: We run this during checkpoints even if logical decoding is disabled so
- * we cleanup old slots at some point after it got disabled.
+ * NB: We run this even if logical decoding is disabled so we cleanup old slots
+ * at some point after it got disabled.
  */
 void
-CheckPointSnapBuild(void)
+RemoveOldSerializedSnapshots(void)
 {
 	XLogRecPtr	cutoff;
 	XLogRecPtr	redo;
diff --git a/src/include/postmaster/custodian.h b/src/include/postmaster/custodian.h
index 73d0bc5f02..ab6d4283b9 100644
--- a/src/include/postmaster/custodian.h
+++ b/src/include/postmaster/custodian.h
@@ -18,7 +18,7 @@
  */
 typedef enum CustodianTask
 {
-	FAKE_TASK,						/* placeholder until we have a real task */
+	CUSTODIAN_REMOVE_SERIALIZED_SNAPSHOTS,
 
 	NUM_CUSTODIAN_TASKS,			/* new tasks go above */
 	INVALID_CUSTODIAN_TASK
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 2a697e57c3..9eba403e0c 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -57,7 +57,7 @@ struct ReorderBuffer;
 struct xl_heap_new_cid;
 struct xl_running_xacts;
 
-extern void CheckPointSnapBuild(void);
+extern void RemoveOldSerializedSnapshots(void);
 
 extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  TransactionId xmin_horizon, XLogRecPtr start_lsn,
-- 
2.25.1


--ew6BAiZeqk4r7MaW
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v17-0003-Move-removal-of-old-logical-rewrite-mapping-file.patch"



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* A few patches to clarify snapshot management, part 2
@ 2025-12-18 23:30 Heikki Linnakangas <[email protected]>
  2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
  2025-12-19 15:16 ` Re: A few patches to clarify snapshot management, part 2 Andres Freund <[email protected]>
  0 siblings, 2 replies; 26+ messages in thread

From: Heikki Linnakangas @ 2025-12-18 23:30 UTC (permalink / raw)
  To: pgsql-hackers

This is a continuation of the earlier thread with the same subject [1], 
and related to the CSN work [2].

I'm pretty happy patches 0001-0005. They make the snapshot management 
more clear in many ways:

Patch 0001: Use a proper type for RestoreTransactionSnapshot's PGPROC arg

Minor cleanup, independent of the rest of the patches


Patch 0002: Split SnapshotData into separate structs for each kind of 
snapshot

This implements the long-standing TODO and splits SnapshotData up into 
multiple structs. This makes it more clear which fields are used with 
which kind of snapshot. For example, we now have properly named fields 
for the XID arrays in historic snapshots. Previously, they abused the 
'xip' and 'subxip' arrays to mean something different than what they 
mean in regular MVCC snapshots.

This introduces some new casts between Snapshots and the new 
MVCCSnapshots. I struggled to decide which functions should use the new 
MVCCSnapshot type and which should continue to use Snapshot. It's a 
balancing act between code churn and where we want to have casts. For 
example, PushActiveSnapshot() takes a Snapshot argument, but assumes 
that it's an MVCC snapshot, so it really should take MVCCSnapshot. But 
most of its callers have a Snapshot rather than MVCCSnapshot. Another 
example: the executor assumes that it's dealing with MVCC snapshots, so 
it would be appropriate to use MVCCSnapshot for EState->es_snapshot. But 
it'd cause a lot code churn and casts elsewhere. I think the patch is a 
reasonable middle ground.


Patch 0003: Simplify historic snapshot refcounting

Little refactoring in logical decoding's snapshot tracking.


Patch 0004: Add an explicit 'valid' flag to MVCCSnapshotData

Makes it more clear when the "static" snapshots returned by 
GetTransactionSnapshot(), GetLatestSnapshot() and GetCatalogSnapshot() 
are valid.


Patch 0005: Replace static snapshot pointers with the 'valid' flags

Refactor snapmgr.c a little, taking advantage of the new 'valid' flags


The rest of the patches are less polished, but please take a look. The 
idea is to split MVCCSnapshot further into a "shared" part that contains 
the xmin, xmax and the 'xip' array, and an outer shell that contains the 
mutable 'curcid' field. This allows reusing the shared part in more 
cases, avoiding copying, although I'm not sure what would be a practical 
scenario where it'd make a performance difference. It becomes more 
important with the CSN patch, however, because it adds a cache to the 
shared snapshot struct, which can potentially grow very large.

[1] 
https://www.postgresql.org/message-id/7c56f180-b9e1-481e-8c1d-efa63de3ecbb%40iki.fi
[2] 
https://www.postgresql.org/message-id/80f254d3-8ee9-4cde-a7e3-ee99998154da%40iki.fi

- Heikki


Attachments:

  [text/x-patch] 0001-Use-a-proper-type-for-RestoreTransactionSnapshot-s-P.patch (1.7K, ../../[email protected]/2-0001-Use-a-proper-type-for-RestoreTransactionSnapshot-s-P.patch)
  download | inline diff:
From f60f39ad07154cd37eeb12355349aa75fc8e6239 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 18 Dec 2025 22:25:59 +0200
Subject: [PATCH 1/9] Use a proper type for RestoreTransactionSnapshot's PGPROC
 arg

---
 src/backend/utils/time/snapmgr.c | 5 +----
 src/include/utils/snapmgr.h      | 3 ++-
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 40a2e90e071..5af8326d5e8 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -1848,12 +1848,9 @@ RestoreSnapshot(char *start_address)
 
 /*
  * Install a restored snapshot as the transaction snapshot.
- *
- * The second argument is of type void * so that snapmgr.h need not include
- * the declaration for PGPROC.
  */
 void
-RestoreTransactionSnapshot(Snapshot snapshot, void *source_pgproc)
+RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc)
 {
 	SetTransactionSnapshot(snapshot, NULL, InvalidPid, source_pgproc);
 }
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index 604c1f90216..b663d3bbc8c 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -120,6 +120,7 @@ extern bool HistoricSnapshotActive(void);
 extern Size EstimateSnapshotSpace(Snapshot snapshot);
 extern void SerializeSnapshot(Snapshot snapshot, char *start_address);
 extern Snapshot RestoreSnapshot(char *start_address);
-extern void RestoreTransactionSnapshot(Snapshot snapshot, void *source_pgproc);
+struct PGPROC;
+extern void RestoreTransactionSnapshot(Snapshot snapshot, struct PGPROC *source_pgproc);
 
 #endif							/* SNAPMGR_H */
-- 
2.47.3



  [text/x-patch] 0002-Split-SnapshotData-into-separate-structs-for-each-ki.patch (86.0K, ../../[email protected]/3-0002-Split-SnapshotData-into-separate-structs-for-each-ki.patch)
  download | inline diff:
From 3bbfeea35698db6c1e5947bef7e9ad7964a1f3e3 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Fri, 20 Dec 2024 00:36:33 +0200
Subject: [PATCH 2/9] Split SnapshotData into separate structs for each kind of
 snapshot

The SnapshotData fields were repurposed for different uses depending
the kind of snapshot. Split it into separate structs for different
kinds of snapshots, so that it is more clear which fields are used
with which snapshot kind, and the fields can have more descriptive
names.
---
 contrib/amcheck/verify_heapam.c               |   2 +-
 contrib/amcheck/verify_nbtree.c               |   2 +-
 src/backend/access/heap/heapam.c              |   3 +-
 src/backend/access/heap/heapam_handler.c      |   6 +-
 src/backend/access/heap/heapam_visibility.c   |  24 +--
 src/backend/access/index/indexam.c            |  11 +-
 src/backend/access/nbtree/nbtinsert.c         |   4 +-
 src/backend/access/spgist/spgvacuum.c         |   2 +-
 src/backend/access/table/tableam.c            |  10 +-
 src/backend/access/transam/parallel.c         |  14 +-
 src/backend/catalog/pg_inherits.c             |   2 +-
 src/backend/commands/async.c                  |   4 +-
 src/backend/commands/indexcmds.c              |   4 +-
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/executor/execIndexing.c           |   4 +-
 src/backend/executor/execReplication.c        |   8 +-
 src/backend/partitioning/partdesc.c           |   2 +-
 src/backend/replication/logical/decode.c      |   2 +-
 src/backend/replication/logical/origin.c      |   4 +-
 .../replication/logical/reorderbuffer.c       | 114 +++++-----
 src/backend/replication/logical/snapbuild.c   | 113 +++++-----
 src/backend/replication/walsender.c           |   2 +-
 src/backend/storage/ipc/procarray.c           |   6 +-
 src/backend/storage/lmgr/predicate.c          |  32 +--
 src/backend/utils/adt/xid8funcs.c             |   4 +-
 src/backend/utils/time/snapmgr.c              | 200 +++++++++++-------
 src/include/access/heapam.h                   |   2 +-
 src/include/access/relscan.h                  |   6 +-
 src/include/replication/reorderbuffer.h       |  12 +-
 src/include/replication/snapbuild.h           |   6 +-
 src/include/replication/snapbuild_internal.h  |   2 +-
 src/include/storage/predicate.h               |   4 +-
 src/include/storage/procarray.h               |   2 +-
 src/include/utils/snapmgr.h                   |  16 +-
 src/include/utils/snapshot.h                  | 155 ++++++++++----
 src/tools/pgindent/typedefs.list              |   4 +
 36 files changed, 453 insertions(+), 337 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..75907349f42 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -310,7 +310,7 @@ verify_heapam(PG_FUNCTION_ARGS)
 	 * Any xmin newer than the xmin of our snapshot can't become all-visible
 	 * while we're running.
 	 */
-	ctx.safe_xmin = GetTransactionSnapshot()->xmin;
+	ctx.safe_xmin = GetTransactionSnapshot()->mvcc.xmin;
 
 	/*
 	 * If we report corruption when not examining some individual attribute,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index f91392a3a49..b91ac1f3fdf 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -454,7 +454,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin &&
 			!TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data),
-								   state->snapshot->xmin))
+								   state->snapshot->mvcc.xmin))
 			ereport(ERROR,
 					errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 					errmsg("index \"%s\" cannot be verified using transaction snapshot",
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6daf4a87dec..1fa424fa131 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -616,7 +616,8 @@ heap_prepare_pagescan(TableScanDesc sscan)
 	 * full page write. Until we can prove that beyond doubt, let's check each
 	 * tuple for visibility the hard way.
 	 */
-	all_visible = PageIsAllVisible(page) && !snapshot->takenDuringRecovery;
+	all_visible = PageIsAllVisible(page) &&
+		(snapshot->snapshot_type != SNAPSHOT_MVCC || !snapshot->mvcc.takenDuringRecovery);
 	check_serializable =
 		CheckForSerializableConflictOutNeeded(scan->rs_base.rs_rd, snapshot);
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index dd4fe6bf62f..26b72a88f65 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -390,7 +390,7 @@ tuple_lock_retry:
 
 		if (!ItemPointerEquals(&tmfd->ctid, &tuple->t_self))
 		{
-			SnapshotData SnapshotDirty;
+			DirtySnapshotData SnapshotDirty;
 			TransactionId priorXmax;
 
 			/* it was updated, so look at the updated version */
@@ -415,7 +415,7 @@ tuple_lock_retry:
 							 errmsg("tuple to be locked was already moved to another partition due to concurrent update")));
 
 				tuple->t_self = *tid;
-				if (heap_fetch(relation, &SnapshotDirty, tuple, &buffer, true))
+				if (heap_fetch(relation, (Snapshot) &SnapshotDirty, tuple, &buffer, true))
 				{
 					/*
 					 * If xmin isn't what we're expecting, the slot must have
@@ -2282,7 +2282,7 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
 
 	page = BufferGetPage(hscan->rs_cbuf);
 	all_visible = PageIsAllVisible(page) &&
-		!scan->rs_snapshot->takenDuringRecovery;
+		(scan->rs_snapshot->snapshot_type != SNAPSHOT_MVCC || !scan->rs_snapshot->mvcc.takenDuringRecovery);
 	maxoffset = PageGetMaxOffsetNumber(page);
 
 	for (;;)
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index 05f6946fe60..f5d69b558f1 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -740,7 +740,7 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid,
  * token is also returned in snapshot->speculativeToken.
  */
 static bool
-HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesDirty(HeapTuple htup, DirtySnapshotData *snapshot,
 						Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -957,7 +957,7 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
  * and more contention on ProcArrayLock.
  */
 static bool
-HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 					   Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -1435,7 +1435,7 @@ HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *de
  *	snapshot->vistest must have been set up with the horizon to use.
  */
 static bool
-HeapTupleSatisfiesNonVacuumable(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesNonVacuumable(HeapTuple htup, NonVacuumableSnapshotData *snapshot,
 								Buffer buffer)
 {
 	TransactionId dead_after = InvalidTransactionId;
@@ -1593,7 +1593,7 @@ TransactionIdInArray(TransactionId xid, TransactionId *xip, Size num)
  * complicated than when dealing "only" with the present.
  */
 static bool
-HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, HistoricMVCCSnapshot snapshot,
 							   Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -1610,7 +1610,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 		return false;
 	}
 	/* check if it's one of our txids, toplevel is also in there */
-	else if (TransactionIdInArray(xmin, snapshot->subxip, snapshot->subxcnt))
+	else if (TransactionIdInArray(xmin, snapshot->curxip, snapshot->curxcnt))
 	{
 		bool		resolved;
 		CommandId	cmin = HeapTupleHeaderGetRawCommandId(tuple);
@@ -1669,7 +1669,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 		return false;
 	}
 	/* check if it's a committed transaction in [xmin, xmax) */
-	else if (TransactionIdInArray(xmin, snapshot->xip, snapshot->xcnt))
+	else if (TransactionIdInArray(xmin, snapshot->committed_xids, snapshot->xcnt))
 	{
 		/* fall through */
 	}
@@ -1702,7 +1702,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 	}
 
 	/* check if it's one of our txids, toplevel is also in there */
-	if (TransactionIdInArray(xmax, snapshot->subxip, snapshot->subxcnt))
+	if (TransactionIdInArray(xmax, snapshot->curxip, snapshot->curxcnt))
 	{
 		bool		resolved;
 		CommandId	cmin;
@@ -1755,7 +1755,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 	else if (TransactionIdFollowsOrEquals(xmax, snapshot->xmax))
 		return true;
 	/* xmax is between [xmin, xmax), check known committed array */
-	else if (TransactionIdInArray(xmax, snapshot->xip, snapshot->xcnt))
+	else if (TransactionIdInArray(xmax, snapshot->committed_xids, snapshot->xcnt))
 		return false;
 	/* xmax is between [xmin, xmax), but known not to have committed yet */
 	else
@@ -1778,7 +1778,7 @@ HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer)
 	switch (snapshot->snapshot_type)
 	{
 		case SNAPSHOT_MVCC:
-			return HeapTupleSatisfiesMVCC(htup, snapshot, buffer);
+			return HeapTupleSatisfiesMVCC(htup, &snapshot->mvcc, buffer);
 		case SNAPSHOT_SELF:
 			return HeapTupleSatisfiesSelf(htup, snapshot, buffer);
 		case SNAPSHOT_ANY:
@@ -1786,11 +1786,11 @@ HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer)
 		case SNAPSHOT_TOAST:
 			return HeapTupleSatisfiesToast(htup, snapshot, buffer);
 		case SNAPSHOT_DIRTY:
-			return HeapTupleSatisfiesDirty(htup, snapshot, buffer);
+			return HeapTupleSatisfiesDirty(htup, &snapshot->dirty, buffer);
 		case SNAPSHOT_HISTORIC_MVCC:
-			return HeapTupleSatisfiesHistoricMVCC(htup, snapshot, buffer);
+			return HeapTupleSatisfiesHistoricMVCC(htup, &snapshot->historic_mvcc, buffer);
 		case SNAPSHOT_NON_VACUUMABLE:
-			return HeapTupleSatisfiesNonVacuumable(htup, snapshot, buffer);
+			return HeapTupleSatisfiesNonVacuumable(htup, &snapshot->nonvacuumable, buffer);
 	}
 
 	return false;				/* keep compiler quiet */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 0492d92d23b..7ef5031d716 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -479,7 +479,7 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	RELATION_CHECKS;
 
 	nbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data);
-	nbytes = add_size(nbytes, EstimateSnapshotSpace(snapshot));
+	nbytes = add_size(nbytes, EstimateSnapshotSpace(&snapshot->mvcc));
 	nbytes = MAXALIGN(nbytes);
 
 	if (instrument)
@@ -528,16 +528,17 @@ index_parallelscan_initialize(Relation heapRelation, Relation indexRelation,
 	Assert(instrument || parallel_aware);
 
 	RELATION_CHECKS;
+	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
 
 	offset = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data),
-					  EstimateSnapshotSpace(snapshot));
+					  EstimateSnapshotSpace((MVCCSnapshot) snapshot));
 	offset = MAXALIGN(offset);
 
 	target->ps_locator = heapRelation->rd_locator;
 	target->ps_indexlocator = indexRelation->rd_locator;
 	target->ps_offset_ins = 0;
 	target->ps_offset_am = 0;
-	SerializeSnapshot(snapshot, target->ps_snapshot_data);
+	SerializeSnapshot((MVCCSnapshot) snapshot, target->ps_snapshot_data);
 
 	if (instrument)
 	{
@@ -601,8 +602,8 @@ index_beginscan_parallel(Relation heaprel, Relation indexrel,
 	Assert(RelFileLocatorEquals(heaprel->rd_locator, pscan->ps_locator));
 	Assert(RelFileLocatorEquals(indexrel->rd_locator, pscan->ps_indexlocator));
 
-	snapshot = RestoreSnapshot(pscan->ps_snapshot_data);
-	RegisterSnapshot(snapshot);
+	snapshot = (Snapshot) RestoreSnapshot(pscan->ps_snapshot_data);
+	snapshot = RegisterSnapshot(snapshot);
 	scan = index_beginscan_internal(indexrel, nkeys, norderbys, snapshot,
 									pscan, true);
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 031eb76ba8c..ae91a30afc9 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -415,7 +415,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 	IndexTuple	curitup = NULL;
 	ItemId		curitemid = NULL;
 	BTScanInsert itup_key = insertstate->itup_key;
-	SnapshotData SnapshotDirty;
+	DirtySnapshotData SnapshotDirty;
 	OffsetNumber offset;
 	OffsetNumber maxoff;
 	Page		page;
@@ -560,7 +560,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 				 * index entry for the entire chain.
 				 */
 				else if (table_index_fetch_tuple_check(heapRel, &htid,
-													   &SnapshotDirty,
+													   (Snapshot) &SnapshotDirty,
 													   &all_dead))
 				{
 					TransactionId xwait;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index cb5671c1a4e..4bd25e50606 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -808,7 +808,7 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	/* Finish setting up spgBulkDeleteState */
 	initSpGistState(&bds->spgstate, index);
 	bds->pendingList = NULL;
-	bds->myXmin = GetActiveSnapshot()->xmin;
+	bds->myXmin = GetActiveSnapshot()->mvcc.xmin;
 	bds->lastFilledBlock = SPGIST_LAST_FIXED_BLKNO;
 
 	/*
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 73ebc01a08f..fde28accfd3 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -133,7 +133,7 @@ table_parallelscan_estimate(Relation rel, Snapshot snapshot)
 	Size		sz = 0;
 
 	if (IsMVCCSnapshot(snapshot))
-		sz = add_size(sz, EstimateSnapshotSpace(snapshot));
+		sz = add_size(sz, EstimateSnapshotSpace((MVCCSnapshot) snapshot));
 	else
 		Assert(snapshot == SnapshotAny);
 
@@ -152,7 +152,7 @@ table_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan,
 
 	if (IsMVCCSnapshot(snapshot))
 	{
-		SerializeSnapshot(snapshot, (char *) pscan + pscan->phs_snapshot_off);
+		SerializeSnapshot((MVCCSnapshot) snapshot, (char *) pscan + pscan->phs_snapshot_off);
 		pscan->phs_snapshot_any = false;
 	}
 	else
@@ -174,8 +174,8 @@ table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan)
 	if (!pscan->phs_snapshot_any)
 	{
 		/* Snapshot was serialized -- restore it */
-		snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		RegisterSnapshot(snapshot);
+		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
+		snapshot = RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
 	else
@@ -204,7 +204,7 @@ table_beginscan_parallel_tidrange(Relation relation,
 	if (!pscan->phs_snapshot_any)
 	{
 		/* Snapshot was serialized -- restore it */
-		snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
+		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
 		RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 642c61fc55c..1fd2146358d 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -279,10 +279,10 @@ InitializeParallelDSM(ParallelContext *pcxt)
 		shm_toc_estimate_chunk(&pcxt->estimator, combocidlen);
 		if (IsolationUsesXactSnapshot())
 		{
-			tsnaplen = EstimateSnapshotSpace(transaction_snapshot);
+			tsnaplen = EstimateSnapshotSpace((MVCCSnapshot) transaction_snapshot);
 			shm_toc_estimate_chunk(&pcxt->estimator, tsnaplen);
 		}
-		asnaplen = EstimateSnapshotSpace(active_snapshot);
+		asnaplen = EstimateSnapshotSpace((MVCCSnapshot) active_snapshot);
 		shm_toc_estimate_chunk(&pcxt->estimator, asnaplen);
 		tstatelen = EstimateTransactionStateSpace();
 		shm_toc_estimate_chunk(&pcxt->estimator, tstatelen);
@@ -401,14 +401,14 @@ InitializeParallelDSM(ParallelContext *pcxt)
 		if (IsolationUsesXactSnapshot())
 		{
 			tsnapspace = shm_toc_allocate(pcxt->toc, tsnaplen);
-			SerializeSnapshot(transaction_snapshot, tsnapspace);
+			SerializeSnapshot((MVCCSnapshot) transaction_snapshot, tsnapspace);
 			shm_toc_insert(pcxt->toc, PARALLEL_KEY_TRANSACTION_SNAPSHOT,
 						   tsnapspace);
 		}
 
 		/* Serialize the active snapshot. */
 		asnapspace = shm_toc_allocate(pcxt->toc, asnaplen);
-		SerializeSnapshot(active_snapshot, asnapspace);
+		SerializeSnapshot((MVCCSnapshot) active_snapshot, asnapspace);
 		shm_toc_insert(pcxt->toc, PARALLEL_KEY_ACTIVE_SNAPSHOT, asnapspace);
 
 		/* Provide the handle for per-session segment. */
@@ -1500,9 +1500,9 @@ ParallelWorkerMain(Datum main_arg)
 	 */
 	asnapspace = shm_toc_lookup(toc, PARALLEL_KEY_ACTIVE_SNAPSHOT, false);
 	tsnapspace = shm_toc_lookup(toc, PARALLEL_KEY_TRANSACTION_SNAPSHOT, true);
-	asnapshot = RestoreSnapshot(asnapspace);
-	tsnapshot = tsnapspace ? RestoreSnapshot(tsnapspace) : asnapshot;
-	RestoreTransactionSnapshot(tsnapshot,
+	asnapshot = (Snapshot) RestoreSnapshot(asnapspace);
+	tsnapshot = tsnapspace ? (Snapshot) RestoreSnapshot(tsnapspace) : asnapshot;
+	RestoreTransactionSnapshot((MVCCSnapshot) tsnapshot,
 							   fps->parallel_leader_pgproc);
 	PushActiveSnapshot(asnapshot);
 
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 929bb53b620..b658601bf77 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -148,7 +148,7 @@ find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
 				xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data);
 				snap = GetActiveSnapshot();
 
-				if (!XidInMVCCSnapshot(xmin, snap))
+				if (!XidInMVCCSnapshot(xmin, (MVCCSnapshot) snap))
 				{
 					if (detached_xmin)
 					{
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index eb86402cae4..7c32d28a9b2 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -1996,6 +1996,8 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 										InvalidTransactionId);
 	page_buffer = NotifyCtl->shared->page_buffer[slotno];
 
+	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+
 	do
 	{
 		QueuePosition thisentry = *current;
@@ -2016,7 +2018,7 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 		/* Ignore messages destined for other databases */
 		if (qe->dboid == MyDatabaseId)
 		{
-			if (XidInMVCCSnapshot(qe->xid, snapshot))
+			if (XidInMVCCSnapshot(qe->xid, (MVCCSnapshot) snapshot))
 			{
 				/*
 				 * The source transaction is still in progress, so we can't
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d9cccb6ac18..76a94172200 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1760,7 +1760,7 @@ DefineIndex(Oid tableId,
 	 * they must wait for.  But first, save the snapshot's xmin to use as
 	 * limitXmin for GetCurrentVirtualXIDs().
 	 */
-	limitXmin = snapshot->xmin;
+	limitXmin = snapshot->mvcc.xmin;
 
 	PopActiveSnapshot();
 	UnregisterSnapshot(snapshot);
@@ -4190,7 +4190,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 		 * We can now do away with our active snapshot, we still need to save
 		 * the xmin limit to wait for older snapshots.
 		 */
-		limitXmin = snapshot->xmin;
+		limitXmin = snapshot->mvcc.xmin;
 
 		PopActiveSnapshot();
 		UnregisterSnapshot(snapshot);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..b2d52f610f5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -21474,7 +21474,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
 	 * all such queries are complete (otherwise we would present them with an
 	 * inconsistent view of catalogs).
 	 */
-	WaitForOlderSnapshots(snap->xmin, false);
+	WaitForOlderSnapshots(snap->mvcc.xmin, false);
 
 	DetachPartitionFinalize(rel, partRel, true, InvalidOid);
 
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 0b3a31f1703..44a40416448 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -717,7 +717,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 	int			indnkeyatts = IndexRelationGetNumberOfKeyAttributes(index);
 	IndexScanDesc index_scan;
 	ScanKeyData scankeys[INDEX_MAX_KEYS];
-	SnapshotData DirtySnapshot;
+	DirtySnapshotData DirtySnapshot;
 	int			i;
 	bool		conflict;
 	bool		found_self;
@@ -816,7 +816,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 retry:
 	conflict = false;
 	found_self = false;
-	index_scan = index_beginscan(heap, index, &DirtySnapshot, NULL, indnkeyatts, 0);
+	index_scan = index_beginscan(heap, index, (Snapshot) &DirtySnapshot, NULL, indnkeyatts, 0);
 	index_rescan(index_scan, scankeys, indnkeyatts, NULL, 0);
 
 	while (index_getnext_slot(index_scan, ForwardScanDirection, existing_slot))
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 860f79f9cc1..de01fe57384 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -186,7 +186,7 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 	ScanKeyData skey[INDEX_MAX_KEYS];
 	int			skey_attoff;
 	IndexScanDesc scan;
-	SnapshotData snap;
+	DirtySnapshotData snap;
 	TransactionId xwait;
 	Relation	idxrel;
 	bool		found;
@@ -204,7 +204,7 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 	skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot);
 
 	/* Start an index scan. */
-	scan = index_beginscan(rel, idxrel, &snap, NULL, skey_attoff, 0);
+	scan = index_beginscan(rel, idxrel, (Snapshot) &snap, NULL, skey_attoff, 0);
 
 retry:
 	found = false;
@@ -370,7 +370,7 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode,
 {
 	TupleTableSlot *scanslot;
 	TableScanDesc scan;
-	SnapshotData snap;
+	DirtySnapshotData snap;
 	TypeCacheEntry **eq;
 	TransactionId xwait;
 	bool		found;
@@ -382,7 +382,7 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode,
 
 	/* Start a heap scan. */
 	InitDirtySnapshot(snap);
-	scan = table_beginscan(rel, &snap, 0, NULL);
+	scan = table_beginscan(rel, (Snapshot) &snap, 0, NULL);
 	scanslot = table_slot_create(rel, NULL);
 
 retry:
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 985f48fc34d..a4a3e2b0ab1 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -102,7 +102,7 @@ RelationGetPartitionDesc(Relation rel, bool omit_detached)
 		Assert(TransactionIdIsValid(rel->rd_partdesc_nodetached_xmin));
 		activesnap = GetActiveSnapshot();
 
-		if (!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, activesnap))
+		if (!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, &activesnap->mvcc))
 			return rel->rd_partdesc_nodetached;
 	}
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 5e15cb1825e..4730d935bac 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -590,7 +590,7 @@ logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	TransactionId xid = XLogRecGetXid(r);
 	uint8		info = XLogRecGetInfo(r) & ~XLR_INFO_MASK;
 	RepOriginId origin_id = XLogRecGetOrigin(r);
-	Snapshot	snapshot = NULL;
+	HistoricMVCCSnapshot snapshot = NULL;
 	xl_logical_message *message;
 
 	if (info != XLOG_LOGICAL_MESSAGE)
diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c
index 2380f369578..2d3ee773f9d 100644
--- a/src/backend/replication/logical/origin.c
+++ b/src/backend/replication/logical/origin.c
@@ -260,7 +260,7 @@ replorigin_create(const char *roname)
 	HeapTuple	tuple = NULL;
 	Relation	rel;
 	Datum		roname_d;
-	SnapshotData SnapshotDirty;
+	DirtySnapshotData SnapshotDirty;
 	SysScanDesc scan;
 	ScanKeyData key;
 
@@ -325,7 +325,7 @@ replorigin_create(const char *roname)
 
 		scan = systable_beginscan(rel, ReplicationOriginIdentIndex,
 								  true /* indexOK */ ,
-								  &SnapshotDirty,
+								  (Snapshot) &SnapshotDirty,
 								  1, &key);
 
 		collides = HeapTupleIsValid(systable_getnext(scan));
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..4d522787129 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -280,9 +280,9 @@ static void ReorderBufferSerializedPath(char *path, ReplicationSlot *slot,
 										TransactionId xid, XLogSegNo segno);
 static int	ReorderBufferTXNSizeCompare(const pairingheap_node *a, const pairingheap_node *b, void *arg);
 
-static void ReorderBufferFreeSnap(ReorderBuffer *rb, Snapshot snap);
-static Snapshot ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
-									  ReorderBufferTXN *txn, CommandId cid);
+static void ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap);
+static HistoricMVCCSnapshot ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
+												  ReorderBufferTXN *txn, CommandId cid);
 
 /*
  * ---------------------------------------
@@ -871,7 +871,7 @@ ReorderBufferQueueChange(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
  */
 void
 ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
-						  Snapshot snap, XLogRecPtr lsn,
+						  HistoricMVCCSnapshot snap, XLogRecPtr lsn,
 						  bool transactional, const char *prefix,
 						  Size message_size, const char *message)
 {
@@ -905,7 +905,7 @@ ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
 	else
 	{
 		ReorderBufferTXN *txn = NULL;
-		volatile Snapshot snapshot_now = snap;
+		volatile	HistoricMVCCSnapshot snapshot_now = snap;
 
 		/* Non-transactional changes require a valid snapshot. */
 		Assert(snapshot_now);
@@ -1905,55 +1905,55 @@ ReorderBufferBuildTupleCidHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
  * that catalog modifying transactions can look into intermediate catalog
  * states.
  */
-static Snapshot
-ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
+static HistoricMVCCSnapshot
+ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 					  ReorderBufferTXN *txn, CommandId cid)
 {
-	Snapshot	snap;
+	HistoricMVCCSnapshot snap;
 	dlist_iter	iter;
 	int			i = 0;
 	Size		size;
 
-	size = sizeof(SnapshotData) +
+	size = sizeof(HistoricMVCCSnapshotData) +
 		sizeof(TransactionId) * orig_snap->xcnt +
 		sizeof(TransactionId) * (txn->nsubtxns + 1);
 
 	snap = MemoryContextAllocZero(rb->context, size);
-	memcpy(snap, orig_snap, sizeof(SnapshotData));
+	memcpy(snap, orig_snap, sizeof(HistoricMVCCSnapshotData));
 
 	snap->copied = true;
-	snap->active_count = 1;		/* mark as active so nobody frees it */
+	snap->refcount = 1;			/* mark as active so nobody frees it */
 	snap->regd_count = 0;
-	snap->xip = (TransactionId *) (snap + 1);
+	snap->committed_xids = (TransactionId *) (snap + 1);
 
-	memcpy(snap->xip, orig_snap->xip, sizeof(TransactionId) * snap->xcnt);
+	memcpy(snap->committed_xids, orig_snap->committed_xids, sizeof(TransactionId) * snap->xcnt);
 
 	/*
-	 * snap->subxip contains all txids that belong to our transaction which we
+	 * snap->curxip contains all txids that belong to our transaction which we
 	 * need to check via cmin/cmax. That's why we store the toplevel
 	 * transaction in there as well.
 	 */
-	snap->subxip = snap->xip + snap->xcnt;
-	snap->subxip[i++] = txn->xid;
+	snap->curxip = snap->committed_xids + snap->xcnt;
+	snap->curxip[i++] = txn->xid;
 
 	/*
 	 * txn->nsubtxns isn't decreased when subtransactions abort, so count
 	 * manually. Since it's an upper boundary it is safe to use it for the
 	 * allocation above.
 	 */
-	snap->subxcnt = 1;
+	snap->curxcnt = 1;
 
 	dlist_foreach(iter, &txn->subtxns)
 	{
 		ReorderBufferTXN *sub_txn;
 
 		sub_txn = dlist_container(ReorderBufferTXN, node, iter.cur);
-		snap->subxip[i++] = sub_txn->xid;
-		snap->subxcnt++;
+		snap->curxip[i++] = sub_txn->xid;
+		snap->curxcnt++;
 	}
 
 	/* sort so we can bsearch() later */
-	qsort(snap->subxip, snap->subxcnt, sizeof(TransactionId), xidComparator);
+	qsort(snap->curxip, snap->curxcnt, sizeof(TransactionId), xidComparator);
 
 	/* store the specified current CommandId */
 	snap->curcid = cid;
@@ -1965,7 +1965,7 @@ ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
  * Free a previously ReorderBufferCopySnap'ed snapshot
  */
 static void
-ReorderBufferFreeSnap(ReorderBuffer *rb, Snapshot snap)
+ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap)
 {
 	if (snap->copied)
 		pfree(snap);
@@ -2118,7 +2118,7 @@ ReorderBufferApplyMessage(ReorderBuffer *rb, ReorderBufferTXN *txn,
  */
 static inline void
 ReorderBufferSaveTXNSnapshot(ReorderBuffer *rb, ReorderBufferTXN *txn,
-							 Snapshot snapshot_now, CommandId command_id)
+							 HistoricMVCCSnapshot snapshot_now, CommandId command_id)
 {
 	txn->command_id = command_id;
 
@@ -2163,7 +2163,7 @@ ReorderBufferMaybeMarkTXNStreamed(ReorderBuffer *rb, ReorderBufferTXN *txn)
  */
 static void
 ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
-					  Snapshot snapshot_now,
+					  HistoricMVCCSnapshot snapshot_now,
 					  CommandId command_id,
 					  XLogRecPtr last_lsn,
 					  ReorderBufferChange *specinsert)
@@ -2210,7 +2210,7 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 static void
 ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 						XLogRecPtr commit_lsn,
-						volatile Snapshot snapshot_now,
+						volatile HistoricMVCCSnapshot snapshot_now,
 						volatile CommandId command_id,
 						bool streaming)
 {
@@ -2826,7 +2826,7 @@ ReorderBufferReplay(ReorderBufferTXN *txn,
 					TimestampTz commit_time,
 					RepOriginId origin_id, XLogRecPtr origin_lsn)
 {
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id = FirstCommandId;
 
 	txn->final_lsn = commit_lsn;
@@ -3306,7 +3306,7 @@ ReorderBufferProcessXid(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn)
  */
 void
 ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
-						 XLogRecPtr lsn, Snapshot snap)
+						 XLogRecPtr lsn, HistoricMVCCSnapshot snap)
 {
 	ReorderBufferChange *change = ReorderBufferAllocChange(rb);
 
@@ -3324,7 +3324,7 @@ ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
  */
 void
 ReorderBufferSetBaseSnapshot(ReorderBuffer *rb, TransactionId xid,
-							 XLogRecPtr lsn, Snapshot snap)
+							 XLogRecPtr lsn, HistoricMVCCSnapshot snap)
 {
 	ReorderBufferTXN *txn;
 	bool		is_new;
@@ -4207,14 +4207,14 @@ ReorderBufferSerializeChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	snap;
+				HistoricMVCCSnapshot snap;
 				char	   *data;
 
 				snap = change->data.snapshot;
 
-				sz += sizeof(SnapshotData) +
+				sz += sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * snap->xcnt +
-					sizeof(TransactionId) * snap->subxcnt;
+					sizeof(TransactionId) * snap->curxcnt;
 
 				/* make sure we have enough space */
 				ReorderBufferSerializeReserve(rb, sz);
@@ -4222,21 +4222,21 @@ ReorderBufferSerializeChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				/* might have been reallocated above */
 				ondisk = (ReorderBufferDiskChange *) rb->outbuf;
 
-				memcpy(data, snap, sizeof(SnapshotData));
-				data += sizeof(SnapshotData);
+				memcpy(data, snap, sizeof(HistoricMVCCSnapshotData));
+				data += sizeof(HistoricMVCCSnapshotData);
 
 				if (snap->xcnt)
 				{
-					memcpy(data, snap->xip,
+					memcpy(data, snap->committed_xids,
 						   sizeof(TransactionId) * snap->xcnt);
 					data += sizeof(TransactionId) * snap->xcnt;
 				}
 
-				if (snap->subxcnt)
+				if (snap->curxcnt)
 				{
-					memcpy(data, snap->subxip,
-						   sizeof(TransactionId) * snap->subxcnt);
-					data += sizeof(TransactionId) * snap->subxcnt;
+					memcpy(data, snap->curxip,
+						   sizeof(TransactionId) * snap->curxcnt);
+					data += sizeof(TransactionId) * snap->curxcnt;
 				}
 				break;
 			}
@@ -4341,7 +4341,7 @@ ReorderBufferCanStartStreaming(ReorderBuffer *rb)
 static void
 ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 {
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id;
 	Size		stream_bytes;
 	bool		txn_is_streamed;
@@ -4360,10 +4360,10 @@ ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	 * After that we need to reuse the snapshot from the previous run.
 	 *
 	 * Unlike DecodeCommit which adds xids of all the subtransactions in
-	 * snapshot's xip array via SnapBuildCommitTxn, we can't do that here but
-	 * we do add them to subxip array instead via ReorderBufferCopySnap. This
-	 * allows the catalog changes made in subtransactions decoded till now to
-	 * be visible.
+	 * snapshot's committed_xids array via SnapBuildCommitTxn, we can't do
+	 * that here but we do add them to curxip array instead via
+	 * ReorderBufferCopySnap. This allows the catalog changes made in
+	 * subtransactions decoded till now to be visible.
 	 */
 	if (txn->snapshot_now == NULL)
 	{
@@ -4509,13 +4509,13 @@ ReorderBufferChangeSize(ReorderBufferChange *change)
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	snap;
+				HistoricMVCCSnapshot snap;
 
 				snap = change->data.snapshot;
 
-				sz += sizeof(SnapshotData) +
+				sz += sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * snap->xcnt +
-					sizeof(TransactionId) * snap->subxcnt;
+					sizeof(TransactionId) * snap->curxcnt;
 
 				break;
 			}
@@ -4793,24 +4793,24 @@ ReorderBufferRestoreChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	oldsnap;
-				Snapshot	newsnap;
+				HistoricMVCCSnapshot oldsnap;
+				HistoricMVCCSnapshot newsnap;
 				Size		size;
 
-				oldsnap = (Snapshot) data;
+				oldsnap = (HistoricMVCCSnapshot) data;
 
-				size = sizeof(SnapshotData) +
+				size = sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * oldsnap->xcnt +
-					sizeof(TransactionId) * (oldsnap->subxcnt + 0);
+					sizeof(TransactionId) * (oldsnap->curxcnt + 0);
 
 				change->data.snapshot = MemoryContextAllocZero(rb->context, size);
 
 				newsnap = change->data.snapshot;
 
 				memcpy(newsnap, data, size);
-				newsnap->xip = (TransactionId *)
-					(((char *) newsnap) + sizeof(SnapshotData));
-				newsnap->subxip = newsnap->xip + newsnap->xcnt;
+				newsnap->committed_xids = (TransactionId *)
+					(((char *) newsnap) + sizeof(HistoricMVCCSnapshotData));
+				newsnap->curxip = newsnap->committed_xids + newsnap->xcnt;
 				newsnap->copied = true;
 				break;
 			}
@@ -5476,7 +5476,7 @@ file_sort_by_lsn(const ListCell *a_p, const ListCell *b_p)
  * transaction for relid.
  */
 static void
-UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
+UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, HistoricMVCCSnapshot snapshot)
 {
 	DIR		   *mapping_dir;
 	struct dirent *mapping_de;
@@ -5524,7 +5524,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
 			continue;
 
 		/* not for our transaction */
-		if (!TransactionIdInArray(f_mapped_xid, snapshot->subxip, snapshot->subxcnt))
+		if (!TransactionIdInArray(f_mapped_xid, snapshot->curxip, snapshot->curxcnt))
 			continue;
 
 		/* ok, relevant, queue for apply */
@@ -5543,7 +5543,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
 		RewriteMappingFile *f = (RewriteMappingFile *) lfirst(file);
 
 		elog(DEBUG1, "applying mapping: \"%s\" in %u", f->fname,
-			 snapshot->subxip[0]);
+			 snapshot->curxip[0]);
 		ApplyLogicalMappingFile(tuplecid_data, relid, f->fname);
 		pfree(f);
 	}
@@ -5555,7 +5555,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
  */
 bool
 ResolveCminCmaxDuringDecoding(HTAB *tuplecid_data,
-							  Snapshot snapshot,
+							  HistoricMVCCSnapshot snapshot,
 							  HeapTuple htup, Buffer buffer,
 							  CommandId *cmin, CommandId *cmax)
 {
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index d6ab1e017eb..80f4b620520 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -155,11 +155,11 @@ static bool ExportInProgress = false;
 static void SnapBuildPurgeOlderTxn(SnapBuild *builder);
 
 /* snapshot building/manipulation/distribution functions */
-static Snapshot SnapBuildBuildSnapshot(SnapBuild *builder);
+static HistoricMVCCSnapshot SnapBuildBuildSnapshot(SnapBuild *builder);
 
-static void SnapBuildFreeSnapshot(Snapshot snap);
+static void SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap);
 
-static void SnapBuildSnapIncRefcount(Snapshot snap);
+static void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
 
 static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
 
@@ -249,23 +249,21 @@ FreeSnapshotBuilder(SnapBuild *builder)
  * Free an unreferenced snapshot that has previously been built by us.
  */
 static void
-SnapBuildFreeSnapshot(Snapshot snap)
+SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap)
 {
 	/* make sure we don't get passed an external snapshot */
 	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
 	/* make sure nobody modified our snapshot */
 	Assert(snap->curcid == FirstCommandId);
-	Assert(!snap->suboverflowed);
-	Assert(!snap->takenDuringRecovery);
 	Assert(snap->regd_count == 0);
 
 	/* slightly more likely, so it's checked even without c-asserts */
 	if (snap->copied)
 		elog(ERROR, "cannot free a copied snapshot");
 
-	if (snap->active_count)
-		elog(ERROR, "cannot free an active snapshot");
+	if (snap->refcount)
+		elog(ERROR, "cannot free a snapshot that's in use");
 
 	pfree(snap);
 }
@@ -313,9 +311,9 @@ SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr)
  * adding a Snapshot as builder->snapshot.
  */
 static void
-SnapBuildSnapIncRefcount(Snapshot snap)
+SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 {
-	snap->active_count++;
+	snap->refcount++;
 }
 
 /*
@@ -325,26 +323,23 @@ SnapBuildSnapIncRefcount(Snapshot snap)
  * IncRef'ed Snapshot can adjust its refcount easily.
  */
 void
-SnapBuildSnapDecRefcount(Snapshot snap)
+SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
 {
 	/* make sure we don't get passed an external snapshot */
 	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
 	/* make sure nobody modified our snapshot */
 	Assert(snap->curcid == FirstCommandId);
-	Assert(!snap->suboverflowed);
-	Assert(!snap->takenDuringRecovery);
 
+	Assert(snap->refcount > 0);
 	Assert(snap->regd_count == 0);
 
-	Assert(snap->active_count > 0);
-
 	/* slightly more likely, so it's checked even without casserts */
 	if (snap->copied)
 		elog(ERROR, "cannot free a copied snapshot");
 
-	snap->active_count--;
-	if (snap->active_count == 0)
+	snap->refcount--;
+	if (snap->refcount == 0)
 		SnapBuildFreeSnapshot(snap);
 }
 
@@ -356,15 +351,15 @@ SnapBuildSnapDecRefcount(Snapshot snap)
  * these snapshots; they have to copy them and fill in appropriate ->curcid
  * and ->subxip/subxcnt values.
  */
-static Snapshot
+static HistoricMVCCSnapshot
 SnapBuildBuildSnapshot(SnapBuild *builder)
 {
-	Snapshot	snapshot;
+	HistoricMVCCSnapshot snapshot;
 	Size		ssize;
 
 	Assert(builder->state >= SNAPBUILD_FULL_SNAPSHOT);
 
-	ssize = sizeof(SnapshotData)
+	ssize = sizeof(HistoricMVCCSnapshotData)
 		+ sizeof(TransactionId) * builder->committed.xcnt
 		+ sizeof(TransactionId) * 1 /* toplevel xid */ ;
 
@@ -400,31 +395,28 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
 	snapshot->xmax = builder->xmax;
 
 	/* store all transactions to be treated as committed by this snapshot */
-	snapshot->xip =
-		(TransactionId *) ((char *) snapshot + sizeof(SnapshotData));
+	snapshot->committed_xids =
+		(TransactionId *) ((char *) snapshot + sizeof(HistoricMVCCSnapshotData));
 	snapshot->xcnt = builder->committed.xcnt;
-	memcpy(snapshot->xip,
+	memcpy(snapshot->committed_xids,
 		   builder->committed.xip,
 		   builder->committed.xcnt * sizeof(TransactionId));
 
 	/* sort so we can bsearch() */
-	qsort(snapshot->xip, snapshot->xcnt, sizeof(TransactionId), xidComparator);
+	qsort(snapshot->committed_xids, snapshot->xcnt, sizeof(TransactionId), xidComparator);
 
 	/*
-	 * Initially, subxip is empty, i.e. it's a snapshot to be used by
+	 * Initially, curxip is empty, i.e. it's a snapshot to be used by
 	 * transactions that don't modify the catalog. Will be filled by
 	 * ReorderBufferCopySnap() if necessary.
 	 */
-	snapshot->subxcnt = 0;
-	snapshot->subxip = NULL;
+	snapshot->curxcnt = 0;
+	snapshot->curxip = NULL;
 
-	snapshot->suboverflowed = false;
-	snapshot->takenDuringRecovery = false;
 	snapshot->copied = false;
 	snapshot->curcid = FirstCommandId;
-	snapshot->active_count = 0;
+	snapshot->refcount = 0;
 	snapshot->regd_count = 0;
-	snapshot->snapXactCompletionCount = 0;
 
 	return snapshot;
 }
@@ -436,13 +428,13 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
  * The snapshot will be usable directly in current transaction or exported
  * for loading in different transaction.
  */
-Snapshot
+MVCCSnapshot
 SnapBuildInitialSnapshot(SnapBuild *builder)
 {
-	Snapshot	snap;
+	HistoricMVCCSnapshot historicsnap;
+	MVCCSnapshot mvccsnap;
 	TransactionId xid;
 	TransactionId safeXid;
-	TransactionId *newxip;
 	int			newxcnt = 0;
 
 	Assert(XactIsoLevel == XACT_REPEATABLE_READ);
@@ -464,10 +456,10 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	if (TransactionIdIsValid(MyProc->xmin))
 		elog(ERROR, "cannot build an initial slot snapshot when MyProc->xmin already is valid");
 
-	snap = SnapBuildBuildSnapshot(builder);
+	historicsnap = SnapBuildBuildSnapshot(builder);
 
 	/*
-	 * We know that snap->xmin is alive, enforced by the logical xmin
+	 * We know that historicsnap->xmin is alive, enforced by the logical xmin
 	 * mechanism. Due to that we can do this without locks, we're only
 	 * changing our own value.
 	 *
@@ -479,14 +471,18 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	safeXid = GetOldestSafeDecodingTransactionId(false);
 	LWLockRelease(ProcArrayLock);
 
-	if (TransactionIdFollows(safeXid, snap->xmin))
+	if (TransactionIdFollows(safeXid, historicsnap->xmin))
 		elog(ERROR, "cannot build an initial slot snapshot as oldest safe xid %u follows snapshot's xmin %u",
-			 safeXid, snap->xmin);
+			 safeXid, historicsnap->xmin);
 
-	MyProc->xmin = snap->xmin;
+	MyProc->xmin = historicsnap->xmin;
 
 	/* allocate in transaction context */
-	newxip = palloc_array(TransactionId, GetMaxSnapshotXidCount());
+	mvccsnap = palloc(sizeof(MVCCSnapshotData) + sizeof(TransactionId) * GetMaxSnapshotXidCount());
+	mvccsnap->snapshot_type = SNAPSHOT_MVCC;
+	mvccsnap->xmin = historicsnap->xmin;
+	mvccsnap->xmax = historicsnap->xmax;
+	mvccsnap->xip = (TransactionId *) ((char *) mvccsnap + sizeof(MVCCSnapshotData));
 
 	/*
 	 * snapbuild.c builds transactions in an "inverted" manner, which means it
@@ -494,15 +490,15 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	 * classical snapshot by marking all non-committed transactions as
 	 * in-progress. This can be expensive.
 	 */
-	for (xid = snap->xmin; NormalTransactionIdPrecedes(xid, snap->xmax);)
+	for (xid = historicsnap->xmin; NormalTransactionIdPrecedes(xid, historicsnap->xmax);)
 	{
 		void	   *test;
 
 		/*
-		 * Check whether transaction committed using the decoding snapshot
-		 * meaning of ->xip.
+		 * Check whether transaction committed using the decoding snapshot's
+		 * committed_xids array.
 		 */
-		test = bsearch(&xid, snap->xip, snap->xcnt,
+		test = bsearch(&xid, historicsnap->committed_xids, historicsnap->xcnt,
 					   sizeof(TransactionId), xidComparator);
 
 		if (test == NULL)
@@ -512,18 +508,27 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 						 errmsg("initial slot snapshot too large")));
 
-			newxip[newxcnt++] = xid;
+			mvccsnap->xip[newxcnt++] = xid;
 		}
 
 		TransactionIdAdvance(xid);
 	}
-
-	/* adjust remaining snapshot fields as needed */
-	snap->snapshot_type = SNAPSHOT_MVCC;
-	snap->xcnt = newxcnt;
-	snap->xip = newxip;
-
-	return snap;
+	mvccsnap->xcnt = newxcnt;
+
+	/* Initialize remaining MVCCSnapshot fields */
+	mvccsnap->subxip = NULL;
+	mvccsnap->subxcnt = 0;
+	mvccsnap->suboverflowed = false;
+	mvccsnap->takenDuringRecovery = false;
+	mvccsnap->copied = true;
+	mvccsnap->curcid = FirstCommandId;
+	mvccsnap->active_count = 0;
+	mvccsnap->regd_count = 0;
+	mvccsnap->snapXactCompletionCount = 0;
+
+	pfree(historicsnap);
+
+	return mvccsnap;
 }
 
 /*
@@ -537,7 +542,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 const char *
 SnapBuildExportSnapshot(SnapBuild *builder)
 {
-	Snapshot	snap;
+	MVCCSnapshot snap;
 	char	   *snapname;
 
 	if (IsTransactionOrTransactionBlock())
@@ -574,7 +579,7 @@ SnapBuildExportSnapshot(SnapBuild *builder)
 /*
  * Ensure there is a snapshot and if not build one for current transaction.
  */
-Snapshot
+HistoricMVCCSnapshot
 SnapBuildGetOrBuildSnapshot(SnapBuild *builder)
 {
 	Assert(builder->state == SNAPBUILD_CONSISTENT);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 449632ad1aa..ac8a9d33323 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1329,7 +1329,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 		else if (snapshot_action == CRS_USE_SNAPSHOT)
 		{
-			Snapshot	snap;
+			MVCCSnapshot snap;
 
 			snap = SnapBuildInitialSnapshot(ctx->snapshot_builder);
 			RestoreTransactionSnapshot(snap, MyProc);
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index f3a1603204e..2292d8a09af 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2040,7 +2040,7 @@ GetMaxSnapshotSubxidCount(void)
  * least in the case we already hold a snapshot), but that's for another day.
  */
 static bool
-GetSnapshotDataReuse(Snapshot snapshot)
+GetSnapshotDataReuse(MVCCSnapshot snapshot)
 {
 	uint64		curXactCompletionCount;
 
@@ -2119,8 +2119,8 @@ GetSnapshotDataReuse(Snapshot snapshot)
  * Note: this function should probably not be called with an argument that's
  * not statically allocated (see xip allocation below).
  */
-Snapshot
-GetSnapshotData(Snapshot snapshot)
+MVCCSnapshot
+GetSnapshotData(MVCCSnapshot snapshot)
 {
 	ProcArrayStruct *arrayP = procArray;
 	TransactionId *other_xids = ProcGlobal->xids;
diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index 149c69a1873..8d97127668c 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -449,10 +449,10 @@ static void SerialSetActiveSerXmin(TransactionId xid);
 
 static uint32 predicatelock_hash(const void *key, Size keysize);
 static void SummarizeOldestCommittedSxact(void);
-static Snapshot GetSafeSnapshot(Snapshot origSnapshot);
-static Snapshot GetSerializableTransactionSnapshotInt(Snapshot snapshot,
-													  VirtualTransactionId *sourcevxid,
-													  int sourcepid);
+static MVCCSnapshot GetSafeSnapshot(MVCCSnapshot origSnapshot);
+static MVCCSnapshot GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
+														  VirtualTransactionId *sourcevxid,
+														  int sourcepid);
 static bool PredicateLockExists(const PREDICATELOCKTARGETTAG *targettag);
 static bool GetParentPredicateLockTag(const PREDICATELOCKTARGETTAG *tag,
 									  PREDICATELOCKTARGETTAG *parent);
@@ -1552,10 +1552,10 @@ SummarizeOldestCommittedSxact(void)
  *		for), the passed-in Snapshot pointer should reference a static data
  *		area that can safely be passed to GetSnapshotData.
  */
-static Snapshot
-GetSafeSnapshot(Snapshot origSnapshot)
+static MVCCSnapshot
+GetSafeSnapshot(MVCCSnapshot origSnapshot)
 {
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 
 	Assert(XactReadOnly && XactDeferrable);
 
@@ -1676,8 +1676,8 @@ GetSafeSnapshotBlockingPids(int blocked_pid, int *output, int output_size)
  * always this same pointer; no new snapshot data structure is allocated
  * within this function.
  */
-Snapshot
-GetSerializableTransactionSnapshot(Snapshot snapshot)
+MVCCSnapshot
+GetSerializableTransactionSnapshot(MVCCSnapshot snapshot)
 {
 	Assert(IsolationIsSerializable());
 
@@ -1717,7 +1717,7 @@ GetSerializableTransactionSnapshot(Snapshot snapshot)
  * read-only.
  */
 void
-SetSerializableTransactionSnapshot(Snapshot snapshot,
+SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
 								   VirtualTransactionId *sourcevxid,
 								   int sourcepid)
 {
@@ -1758,8 +1758,8 @@ SetSerializableTransactionSnapshot(Snapshot snapshot,
  * source xact is still running after we acquire SerializableXactHashLock.
  * We do that by calling ProcArrayInstallImportedXmin.
  */
-static Snapshot
-GetSerializableTransactionSnapshotInt(Snapshot snapshot,
+static MVCCSnapshot
+GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 									  VirtualTransactionId *sourcevxid,
 									  int sourcepid)
 {
@@ -3969,12 +3969,12 @@ ReleaseOneSerializableXact(SERIALIZABLEXACT *sxact, bool partial,
 static bool
 XidIsConcurrent(TransactionId xid)
 {
-	Snapshot	snap;
+	MVCCSnapshot snap;
 
 	Assert(TransactionIdIsValid(xid));
 	Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()));
 
-	snap = GetTransactionSnapshot();
+	snap = (MVCCSnapshot) GetTransactionSnapshot();
 
 	if (TransactionIdPrecedes(xid, snap->xmin))
 		return false;
@@ -4222,7 +4222,7 @@ CheckTargetForConflictsIn(PREDICATELOCKTARGETTAG *targettag)
 		}
 		else if (!SxactIsDoomed(sxact)
 				 && (!SxactIsCommitted(sxact)
-					 || TransactionIdPrecedes(GetTransactionSnapshot()->xmin,
+					 || TransactionIdPrecedes(TransactionXmin,
 											  sxact->finishedBefore))
 				 && !RWConflictExists(sxact, MySerializableXact))
 		{
@@ -4235,7 +4235,7 @@ CheckTargetForConflictsIn(PREDICATELOCKTARGETTAG *targettag)
 			 */
 			if (!SxactIsDoomed(sxact)
 				&& (!SxactIsCommitted(sxact)
-					|| TransactionIdPrecedes(GetTransactionSnapshot()->xmin,
+					|| TransactionIdPrecedes(TransactionXmin,
 											 sxact->finishedBefore))
 				&& !RWConflictExists(sxact, MySerializableXact))
 			{
diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index 4b3f7a69b3b..0407d16fe12 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -373,10 +373,10 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	pg_snapshot *snap;
 	uint32		nxip,
 				i;
-	Snapshot	cur;
+	MVCCSnapshot cur;
 	FullTransactionId next_fxid = ReadNextFullTransactionId();
 
-	cur = GetActiveSnapshot();
+	cur = (MVCCSnapshot) GetActiveSnapshot();
 	if (cur == NULL)
 		elog(ERROR, "no active snapshot set");
 
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 5af8326d5e8..85395f0a8df 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -138,18 +138,18 @@
  * These SnapshotData structs are static to simplify memory allocation
  * (see the hack in GetSnapshotData to avoid repeated malloc/free).
  */
-static SnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
-static SnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
-static SnapshotData CatalogSnapshotData = {SNAPSHOT_MVCC};
+static MVCCSnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
+static MVCCSnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
+static MVCCSnapshotData CatalogSnapshotData = {SNAPSHOT_MVCC};
 SnapshotData SnapshotSelfData = {SNAPSHOT_SELF};
 SnapshotData SnapshotAnyData = {SNAPSHOT_ANY};
 SnapshotData SnapshotToastData = {SNAPSHOT_TOAST};
 
 /* Pointers to valid snapshots */
-static Snapshot CurrentSnapshot = NULL;
-static Snapshot SecondarySnapshot = NULL;
-static Snapshot CatalogSnapshot = NULL;
-static Snapshot HistoricSnapshot = NULL;
+static MVCCSnapshot CurrentSnapshot = NULL;
+static MVCCSnapshot SecondarySnapshot = NULL;
+static MVCCSnapshot CatalogSnapshot = NULL;
+static HistoricMVCCSnapshot HistoricSnapshot = NULL;
 
 /*
  * These are updated by GetSnapshotData.  We initialize them this way
@@ -172,7 +172,7 @@ static HTAB *tuplecid_data = NULL;
  */
 typedef struct ActiveSnapshotElt
 {
-	Snapshot	as_snap;
+	MVCCSnapshot as_snap;
 	int			as_level;
 	struct ActiveSnapshotElt *as_next;
 } ActiveSnapshotElt;
@@ -197,7 +197,7 @@ bool		FirstSnapshotSet = false;
  * FirstSnapshotSet in combination with IsolationUsesXactSnapshot(), because
  * GUC may be reset before us, changing the value of IsolationUsesXactSnapshot.
  */
-static Snapshot FirstXactSnapshot = NULL;
+static MVCCSnapshot FirstXactSnapshot = NULL;
 
 /* Define pathname of exported-snapshot files */
 #define SNAPSHOT_EXPORT_DIR "pg_snapshots"
@@ -206,16 +206,16 @@ static Snapshot FirstXactSnapshot = NULL;
 typedef struct ExportedSnapshot
 {
 	char	   *snapfile;
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 } ExportedSnapshot;
 
 /* Current xact's exported snapshots (a list of ExportedSnapshot structs) */
 static List *exportedSnapshots = NIL;
 
 /* Prototypes for local functions */
-static Snapshot CopySnapshot(Snapshot snapshot);
+static MVCCSnapshot CopyMVCCSnapshot(MVCCSnapshot snapshot);
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
-static void FreeSnapshot(Snapshot snapshot);
+static void FreeMVCCSnapshot(MVCCSnapshot snapshot);
 static void SnapshotResetXmin(void);
 
 /* ResourceOwner callbacks to track snapshot references */
@@ -287,7 +287,7 @@ GetTransactionSnapshot(void)
 		 * for later calls to GetTransactionSnapshot().
 		 */
 		Assert(!FirstSnapshotSet);
-		return HistoricSnapshot;
+		return (Snapshot) HistoricSnapshot;
 	}
 
 	/* First call in transaction? */
@@ -320,8 +320,9 @@ GetTransactionSnapshot(void)
 				CurrentSnapshot = GetSerializableTransactionSnapshot(&CurrentSnapshotData);
 			else
 				CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+
 			/* Make a saved copy */
-			CurrentSnapshot = CopySnapshot(CurrentSnapshot);
+			CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
 			FirstXactSnapshot = CurrentSnapshot;
 			/* Mark it as "registered" in FirstXactSnapshot */
 			FirstXactSnapshot->regd_count++;
@@ -331,18 +332,18 @@ GetTransactionSnapshot(void)
 			CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
 
 		FirstSnapshotSet = true;
-		return CurrentSnapshot;
+		return (Snapshot) CurrentSnapshot;
 	}
 
 	if (IsolationUsesXactSnapshot())
-		return CurrentSnapshot;
+		return (Snapshot) CurrentSnapshot;
 
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
 	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
 
-	return CurrentSnapshot;
+	return (Snapshot) CurrentSnapshot;
 }
 
 /*
@@ -373,7 +374,7 @@ GetLatestSnapshot(void)
 
 	SecondarySnapshot = GetSnapshotData(&SecondarySnapshotData);
 
-	return SecondarySnapshot;
+	return (Snapshot) SecondarySnapshot;
 }
 
 /*
@@ -392,7 +393,7 @@ GetCatalogSnapshot(Oid relid)
 	 * finishing decoding.
 	 */
 	if (HistoricSnapshotActive())
-		return HistoricSnapshot;
+		return (Snapshot) HistoricSnapshot;
 
 	return GetNonHistoricCatalogSnapshot(relid);
 }
@@ -438,7 +439,7 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
 	}
 
-	return CatalogSnapshot;
+	return (Snapshot) CatalogSnapshot;
 }
 
 /*
@@ -508,7 +509,7 @@ SnapshotSetCommandId(CommandId curcid)
  * in GetTransactionSnapshot.
  */
 static void
-SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
+SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid,
 					   int sourcepid, PGPROC *sourceproc)
 {
 	/* Caller should have checked this already */
@@ -587,7 +588,7 @@ SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
 			SetSerializableTransactionSnapshot(CurrentSnapshot, sourcevxid,
 											   sourcepid);
 		/* Make a saved copy */
-		CurrentSnapshot = CopySnapshot(CurrentSnapshot);
+		CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
 		FirstXactSnapshot = CurrentSnapshot;
 		/* Mark it as "registered" in FirstXactSnapshot */
 		FirstXactSnapshot->regd_count++;
@@ -598,29 +599,27 @@ SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
 }
 
 /*
- * CopySnapshot
+ * CopyMVCCSnapshot
  *		Copy the given snapshot.
  *
  * The copy is palloc'd in TopTransactionContext and has initial refcounts set
  * to 0.  The returned snapshot has the copied flag set.
  */
-static Snapshot
-CopySnapshot(Snapshot snapshot)
+static MVCCSnapshot
+CopyMVCCSnapshot(MVCCSnapshot snapshot)
 {
-	Snapshot	newsnap;
+	MVCCSnapshot newsnap;
 	Size		subxipoff;
 	Size		size;
 
-	Assert(snapshot != InvalidSnapshot);
-
 	/* We allocate any XID arrays needed in the same palloc block. */
-	size = subxipoff = sizeof(SnapshotData) +
+	size = subxipoff = sizeof(MVCCSnapshotData) +
 		snapshot->xcnt * sizeof(TransactionId);
 	if (snapshot->subxcnt > 0)
 		size += snapshot->subxcnt * sizeof(TransactionId);
 
-	newsnap = (Snapshot) MemoryContextAlloc(TopTransactionContext, size);
-	memcpy(newsnap, snapshot, sizeof(SnapshotData));
+	newsnap = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
+	memcpy(newsnap, snapshot, sizeof(MVCCSnapshotData));
 
 	newsnap->regd_count = 0;
 	newsnap->active_count = 0;
@@ -657,11 +656,11 @@ CopySnapshot(Snapshot snapshot)
 }
 
 /*
- * FreeSnapshot
+ * FreeMVCCSnapshot
  *		Free the memory associated with a snapshot.
  */
 static void
-FreeSnapshot(Snapshot snapshot)
+FreeMVCCSnapshot(MVCCSnapshot snapshot)
 {
 	Assert(snapshot->regd_count == 0);
 	Assert(snapshot->active_count == 0);
@@ -677,6 +676,8 @@ FreeSnapshot(Snapshot snapshot)
  * If the passed snapshot is a statically-allocated one, or it is possibly
  * subject to a future command counter update, create a new long-lived copy
  * with active refcount=1.  Otherwise, only increment the refcount.
+ *
+ * Only regular MVCC snaphots can be used as the active snapshot.
  */
 void
 PushActiveSnapshot(Snapshot snapshot)
@@ -695,9 +696,12 @@ PushActiveSnapshot(Snapshot snapshot)
 void
 PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 {
+	MVCCSnapshot origsnap;
 	ActiveSnapshotElt *newactive;
 
-	Assert(snapshot != InvalidSnapshot);
+	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+	origsnap = &snapshot->mvcc;
+
 	Assert(ActiveSnapshot == NULL || snap_level >= ActiveSnapshot->as_level);
 
 	newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
@@ -706,11 +710,11 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 	 * Checking SecondarySnapshot is probably useless here, but it seems
 	 * better to be sure.
 	 */
-	if (snapshot == CurrentSnapshot || snapshot == SecondarySnapshot ||
-		!snapshot->copied)
-		newactive->as_snap = CopySnapshot(snapshot);
+	if (origsnap == CurrentSnapshot || origsnap == SecondarySnapshot ||
+		!origsnap->copied)
+		newactive->as_snap = CopyMVCCSnapshot(origsnap);
 	else
-		newactive->as_snap = snapshot;
+		newactive->as_snap = origsnap;
 
 	newactive->as_next = ActiveSnapshot;
 	newactive->as_level = snap_level;
@@ -731,7 +735,8 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 void
 PushCopiedSnapshot(Snapshot snapshot)
 {
-	PushActiveSnapshot(CopySnapshot(snapshot));
+	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+	PushActiveSnapshot((Snapshot) CopyMVCCSnapshot(&snapshot->mvcc));
 }
 
 /*
@@ -784,7 +789,7 @@ PopActiveSnapshot(void)
 
 	if (ActiveSnapshot->as_snap->active_count == 0 &&
 		ActiveSnapshot->as_snap->regd_count == 0)
-		FreeSnapshot(ActiveSnapshot->as_snap);
+		FreeMVCCSnapshot(ActiveSnapshot->as_snap);
 
 	pfree(ActiveSnapshot);
 	ActiveSnapshot = newstack;
@@ -801,7 +806,7 @@ GetActiveSnapshot(void)
 {
 	Assert(ActiveSnapshot != NULL);
 
-	return ActiveSnapshot->as_snap;
+	return (Snapshot) ActiveSnapshot->as_snap;
 }
 
 /*
@@ -818,7 +823,8 @@ ActiveSnapshotSet(void)
  * RegisterSnapshot
  *		Register a snapshot as being in use by the current resource owner
  *
- * If InvalidSnapshot is passed, it is not registered.
+ * Only regular MVCC snaphots and "historic" MVCC snapshots can be registered.
+ * InvalidSnapshot is also accepted, as a no-op.
  */
 Snapshot
 RegisterSnapshot(Snapshot snapshot)
@@ -834,25 +840,39 @@ RegisterSnapshot(Snapshot snapshot)
  *		As above, but use the specified resource owner
  */
 Snapshot
-RegisterSnapshotOnOwner(Snapshot snapshot, ResourceOwner owner)
+RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 {
-	Snapshot	snap;
+	MVCCSnapshot snapshot;
 
-	if (snapshot == InvalidSnapshot)
+	if (orig_snapshot == InvalidSnapshot)
 		return InvalidSnapshot;
 
+	if (orig_snapshot->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	{
+		HistoricMVCCSnapshot historicsnap = &orig_snapshot->historic_mvcc;
+
+		ResourceOwnerEnlarge(owner);
+		historicsnap->regd_count++;
+		ResourceOwnerRememberSnapshot(owner, (Snapshot) historicsnap);
+
+		return (Snapshot) historicsnap;
+	}
+
+	Assert(orig_snapshot->snapshot_type == SNAPSHOT_MVCC);
+	snapshot = &orig_snapshot->mvcc;
+
 	/* Static snapshot?  Create a persistent copy */
-	snap = snapshot->copied ? snapshot : CopySnapshot(snapshot);
+	snapshot = snapshot->copied ? snapshot : CopyMVCCSnapshot(snapshot);
 
 	/* and tell resowner.c about it */
 	ResourceOwnerEnlarge(owner);
-	snap->regd_count++;
-	ResourceOwnerRememberSnapshot(owner, snap);
+	snapshot->regd_count++;
+	ResourceOwnerRememberSnapshot(owner, (Snapshot) snapshot);
 
-	if (snap->regd_count == 1)
-		pairingheap_add(&RegisteredSnapshots, &snap->ph_node);
+	if (snapshot->regd_count == 1)
+		pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
 
-	return snap;
+	return (Snapshot) snapshot;
 }
 
 /*
@@ -888,18 +908,41 @@ UnregisterSnapshotFromOwner(Snapshot snapshot, ResourceOwner owner)
 static void
 UnregisterSnapshotNoOwner(Snapshot snapshot)
 {
-	Assert(snapshot->regd_count > 0);
-	Assert(!pairingheap_is_empty(&RegisteredSnapshots));
+	if (snapshot->snapshot_type == SNAPSHOT_MVCC)
+	{
+		MVCCSnapshot mvccsnap = &snapshot->mvcc;
+
+		Assert(mvccsnap->regd_count > 0);
+		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
 
-	snapshot->regd_count--;
-	if (snapshot->regd_count == 0)
-		pairingheap_remove(&RegisteredSnapshots, &snapshot->ph_node);
+		mvccsnap->regd_count--;
+		if (mvccsnap->regd_count == 0)
+			pairingheap_remove(&RegisteredSnapshots, &mvccsnap->ph_node);
 
-	if (snapshot->regd_count == 0 && snapshot->active_count == 0)
+		if (mvccsnap->regd_count == 0 && mvccsnap->active_count == 0)
+		{
+			FreeMVCCSnapshot(mvccsnap);
+			SnapshotResetXmin();
+		}
+	}
+	else if (snapshot->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
 	{
-		FreeSnapshot(snapshot);
-		SnapshotResetXmin();
+		HistoricMVCCSnapshot historicsnap = &snapshot->historic_mvcc;
+
+		/*
+		 * Historic snapshots don't rely on the resource owner machinery for
+		 * cleanup, the snapbuild.c machinery ensures that whenever a historic
+		 * snapshot is in use, it has a non-zero refcount.  Registration is
+		 * only supported so that the callers don't need to treat regular MVCC
+		 * catalog snapshots and historic snapshots differently.
+		 */
+		Assert(historicsnap->refcount > 0);
+
+		Assert(historicsnap->regd_count > 0);
+		historicsnap->regd_count--;
 	}
+	else
+		elog(ERROR, "registered snapshot has unexpected type");
 }
 
 /*
@@ -909,8 +952,8 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 static int
 xmin_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg)
 {
-	const SnapshotData *asnap = pairingheap_const_container(SnapshotData, ph_node, a);
-	const SnapshotData *bsnap = pairingheap_const_container(SnapshotData, ph_node, b);
+	const MVCCSnapshotData *asnap = pairingheap_const_container(MVCCSnapshotData, ph_node, a);
+	const MVCCSnapshotData *bsnap = pairingheap_const_container(MVCCSnapshotData, ph_node, b);
 
 	if (TransactionIdPrecedes(asnap->xmin, bsnap->xmin))
 		return 1;
@@ -936,7 +979,7 @@ xmin_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg)
 static void
 SnapshotResetXmin(void)
 {
-	Snapshot	minSnapshot;
+	MVCCSnapshot minSnapshot;
 
 	if (ActiveSnapshot != NULL)
 		return;
@@ -947,7 +990,7 @@ SnapshotResetXmin(void)
 		return;
 	}
 
-	minSnapshot = pairingheap_container(SnapshotData, ph_node,
+	minSnapshot = pairingheap_container(MVCCSnapshotData, ph_node,
 										pairingheap_first(&RegisteredSnapshots));
 
 	if (TransactionIdPrecedes(MyProc->xmin, minSnapshot->xmin))
@@ -997,7 +1040,7 @@ AtSubAbort_Snapshot(int level)
 
 		if (ActiveSnapshot->as_snap->active_count == 0 &&
 			ActiveSnapshot->as_snap->regd_count == 0)
-			FreeSnapshot(ActiveSnapshot->as_snap);
+			FreeMVCCSnapshot(ActiveSnapshot->as_snap);
 
 		/* and free the stack element */
 		pfree(ActiveSnapshot);
@@ -1019,7 +1062,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * In transaction-snapshot mode we must release our privately-managed
 	 * reference to the transaction snapshot.  We must remove it from
 	 * RegisteredSnapshots to keep the check below happy.  But we don't bother
-	 * to do FreeSnapshot, for two reasons: the memory will go away with
+	 * to do FreeMVCCSnapshot, for two reasons: the memory will go away with
 	 * TopTransactionContext anyway, and if someone has left the snapshot
 	 * stacked as active, we don't want the code below to be chasing through a
 	 * dangling pointer.
@@ -1112,7 +1155,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
  *		snapshot.
  */
 char *
-ExportSnapshot(Snapshot snapshot)
+ExportSnapshot(MVCCSnapshot snapshot)
 {
 	TransactionId topXid;
 	TransactionId *children;
@@ -1176,7 +1219,7 @@ ExportSnapshot(Snapshot snapshot)
 	 * ensure that the snapshot's xmin is honored for the rest of the
 	 * transaction.
 	 */
-	snapshot = CopySnapshot(snapshot);
+	snapshot = CopyMVCCSnapshot(snapshot);
 
 	oldcxt = MemoryContextSwitchTo(TopTransactionContext);
 	esnap = palloc_object(ExportedSnapshot);
@@ -1293,7 +1336,7 @@ pg_export_snapshot(PG_FUNCTION_ARGS)
 {
 	char	   *snapshotName;
 
-	snapshotName = ExportSnapshot(GetActiveSnapshot());
+	snapshotName = ExportSnapshot((MVCCSnapshot) GetActiveSnapshot());
 	PG_RETURN_TEXT_P(cstring_to_text(snapshotName));
 }
 
@@ -1397,7 +1440,7 @@ ImportSnapshot(const char *idstr)
 	Oid			src_dbid;
 	int			src_isolevel;
 	bool		src_readonly;
-	SnapshotData snapshot;
+	MVCCSnapshotData snapshot;
 
 	/*
 	 * Must be at top level of a fresh transaction.  Note in particular that
@@ -1666,7 +1709,7 @@ HaveRegisteredOrActiveSnapshot(void)
  * Needed for logical decoding.
  */
 void
-SetupHistoricSnapshot(Snapshot historic_snapshot, HTAB *tuplecids)
+SetupHistoricSnapshot(HistoricMVCCSnapshot historic_snapshot, HTAB *tuplecids)
 {
 	Assert(historic_snapshot != NULL);
 
@@ -1709,11 +1752,10 @@ HistoricSnapshotGetTupleCids(void)
  * SerializedSnapshotData.
  */
 Size
-EstimateSnapshotSpace(Snapshot snapshot)
+EstimateSnapshotSpace(MVCCSnapshot snapshot)
 {
 	Size		size;
 
-	Assert(snapshot != InvalidSnapshot);
 	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
 
 	/* We allocate any XID arrays needed in the same palloc block. */
@@ -1733,7 +1775,7 @@ EstimateSnapshotSpace(Snapshot snapshot)
  *		memory location at start_address.
  */
 void
-SerializeSnapshot(Snapshot snapshot, char *start_address)
+SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 
@@ -1789,12 +1831,12 @@ SerializeSnapshot(Snapshot snapshot, char *start_address)
  * The copy is palloc'd in TopTransactionContext and has initial refcounts set
  * to 0.  The returned snapshot has the copied flag set.
  */
-Snapshot
+MVCCSnapshot
 RestoreSnapshot(char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 	Size		size;
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 	TransactionId *serialized_xids;
 
 	memcpy(&serialized_snapshot, start_address,
@@ -1803,12 +1845,12 @@ RestoreSnapshot(char *start_address)
 		(start_address + sizeof(SerializedSnapshotData));
 
 	/* We allocate any XID arrays needed in the same palloc block. */
-	size = sizeof(SnapshotData)
+	size = sizeof(MVCCSnapshotData)
 		+ serialized_snapshot.xcnt * sizeof(TransactionId)
 		+ serialized_snapshot.subxcnt * sizeof(TransactionId);
 
 	/* Copy all required fields */
-	snapshot = (Snapshot) MemoryContextAlloc(TopTransactionContext, size);
+	snapshot = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
 	snapshot->snapshot_type = SNAPSHOT_MVCC;
 	snapshot->xmin = serialized_snapshot.xmin;
 	snapshot->xmax = serialized_snapshot.xmax;
@@ -1850,7 +1892,7 @@ RestoreSnapshot(char *start_address)
  * Install a restored snapshot as the transaction snapshot.
  */
 void
-RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc)
+RestoreTransactionSnapshot(MVCCSnapshot snapshot, PGPROC *source_pgproc)
 {
 	SetTransactionSnapshot(snapshot, NULL, InvalidPid, source_pgproc);
 }
@@ -1866,7 +1908,7 @@ RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc)
  * XID could not be ours anyway.
  */
 bool
-XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot)
+XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot)
 {
 	/*
 	 * Make a quick range check to eliminate most XIDs without looking at the
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index f7e4ae3843c..b64731f926c 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -455,7 +455,7 @@ extern bool HeapTupleIsSurelyDead(HeapTuple htup,
  */
 struct HTAB;
 extern bool ResolveCminCmaxDuringDecoding(struct HTAB *tuplecid_data,
-										  Snapshot snapshot,
+										  HistoricMVCCSnapshot snapshot,
 										  HeapTuple htup,
 										  Buffer buffer,
 										  CommandId *cmin, CommandId *cmax);
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 78989a959d4..0f8fdcff782 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -34,7 +34,7 @@ typedef struct TableScanDescData
 {
 	/* scan parameters */
 	Relation	rs_rd;			/* heap relation descriptor */
-	struct SnapshotData *rs_snapshot;	/* snapshot to see */
+	union SnapshotData *rs_snapshot;	/* snapshot to see */
 	int			rs_nkeys;		/* number of scan keys */
 	struct ScanKeyData *rs_key; /* array of scan key descriptors */
 
@@ -137,7 +137,7 @@ typedef struct IndexScanDescData
 	/* scan parameters */
 	Relation	heapRelation;	/* heap relation descriptor, or NULL */
 	Relation	indexRelation;	/* index relation descriptor */
-	struct SnapshotData *xs_snapshot;	/* snapshot to see */
+	union SnapshotData *xs_snapshot;	/* snapshot to see */
 	int			numberOfKeys;	/* number of index qualifier conditions */
 	int			numberOfOrderBys;	/* number of ordering operators */
 	struct ScanKeyData *keyData;	/* array of index qualifier descriptors */
@@ -212,7 +212,7 @@ typedef struct SysScanDescData
 	Relation	irel;			/* NULL if doing heap scan */
 	struct TableScanDescData *scan; /* only valid in storage-scan case */
 	struct IndexScanDescData *iscan;	/* only valid in index-scan case */
-	struct SnapshotData *snapshot;	/* snapshot to unregister at end of scan */
+	union SnapshotData *snapshot;	/* snapshot to unregister at end of scan */
 	struct TupleTableSlot *slot;
 } SysScanDescData;
 
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 3cbe106a3c7..bfe14d5144a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -127,7 +127,7 @@ typedef struct ReorderBufferChange
 		}			msg;
 
 		/* New snapshot, set when action == *_INTERNAL_SNAPSHOT */
-		Snapshot	snapshot;
+		HistoricMVCCSnapshot snapshot;
 
 		/*
 		 * New command id for existing snapshot in a catalog changing tx. Set
@@ -366,7 +366,7 @@ typedef struct ReorderBufferTXN
 	 * transaction modifies the catalog, or another catalog-modifying
 	 * transaction commits.
 	 */
-	Snapshot	base_snapshot;
+	HistoricMVCCSnapshot base_snapshot;
 	XLogRecPtr	base_snapshot_lsn;
 	dlist_node	base_snapshot_node; /* link in txns_by_base_snapshot_lsn */
 
@@ -374,7 +374,7 @@ typedef struct ReorderBufferTXN
 	 * Snapshot/CID from the previous streaming run. Only valid for already
 	 * streamed transactions (NULL/InvalidCommandId otherwise).
 	 */
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id;
 
 	/*
@@ -719,7 +719,7 @@ extern void ReorderBufferQueueChange(ReorderBuffer *rb, TransactionId xid,
 									 XLogRecPtr lsn, ReorderBufferChange *change,
 									 bool toast_insert);
 extern void ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
-									  Snapshot snap, XLogRecPtr lsn,
+									  HistoricMVCCSnapshot snap, XLogRecPtr lsn,
 									  bool transactional, const char *prefix,
 									  Size message_size, const char *message);
 extern void ReorderBufferCommit(ReorderBuffer *rb, TransactionId xid,
@@ -743,9 +743,9 @@ extern void ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr
 extern void ReorderBufferInvalidate(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn);
 
 extern void ReorderBufferSetBaseSnapshot(ReorderBuffer *rb, TransactionId xid,
-										 XLogRecPtr lsn, Snapshot snap);
+										 XLogRecPtr lsn, HistoricMVCCSnapshot snap);
 extern void ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
-									 XLogRecPtr lsn, Snapshot snap);
+									 XLogRecPtr lsn, HistoricMVCCSnapshot snap);
 extern void ReorderBufferAddNewCommandId(ReorderBuffer *rb, TransactionId xid,
 										 XLogRecPtr lsn, CommandId cid);
 extern void ReorderBufferAddNewTupleCids(ReorderBuffer *rb, TransactionId xid,
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 44031dcf6e3..5930ffb55a8 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -70,15 +70,15 @@ extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  XLogRecPtr two_phase_at);
 extern void FreeSnapshotBuilder(SnapBuild *builder);
 
-extern void SnapBuildSnapDecRefcount(Snapshot snap);
+extern void SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap);
 
-extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder);
+extern MVCCSnapshot SnapBuildInitialSnapshot(SnapBuild *builder);
 extern const char *SnapBuildExportSnapshot(SnapBuild *builder);
 extern void SnapBuildClearExportedSnapshot(void);
 extern void SnapBuildResetExportedSnapshotState(void);
 
 extern SnapBuildState SnapBuildCurrentState(SnapBuild *builder);
-extern Snapshot SnapBuildGetOrBuildSnapshot(SnapBuild *builder);
+extern HistoricMVCCSnapshot SnapBuildGetOrBuildSnapshot(SnapBuild *builder);
 
 extern bool SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr);
 extern XLogRecPtr SnapBuildGetTwoPhaseAt(SnapBuild *builder);
diff --git a/src/include/replication/snapbuild_internal.h b/src/include/replication/snapbuild_internal.h
index 3b915dc8793..9bed20efa31 100644
--- a/src/include/replication/snapbuild_internal.h
+++ b/src/include/replication/snapbuild_internal.h
@@ -74,7 +74,7 @@ struct SnapBuild
 	/*
 	 * Snapshot that's valid to see the catalog state seen at this moment.
 	 */
-	Snapshot	snapshot;
+	HistoricMVCCSnapshot snapshot;
 
 	/*
 	 * LSN of the last location we are sure a snapshot has been serialized to.
diff --git a/src/include/storage/predicate.h b/src/include/storage/predicate.h
index 8f5f0348a23..9a7f53c900c 100644
--- a/src/include/storage/predicate.h
+++ b/src/include/storage/predicate.h
@@ -47,8 +47,8 @@ extern void CheckPointPredicate(void);
 extern bool PageIsPredicateLocked(Relation relation, BlockNumber blkno);
 
 /* predicate lock maintenance */
-extern Snapshot GetSerializableTransactionSnapshot(Snapshot snapshot);
-extern void SetSerializableTransactionSnapshot(Snapshot snapshot,
+extern MVCCSnapshot GetSerializableTransactionSnapshot(MVCCSnapshot snapshot);
+extern void SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
 											   VirtualTransactionId *sourcevxid,
 											   int sourcepid);
 extern void RegisterPredicateLockingXid(TransactionId xid);
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index 2f4ae06c279..c40c2ee5d1b 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -44,7 +44,7 @@ extern void KnownAssignedTransactionIdsIdleMaintenance(void);
 extern int	GetMaxSnapshotXidCount(void);
 extern int	GetMaxSnapshotSubxidCount(void);
 
-extern Snapshot GetSnapshotData(Snapshot snapshot);
+extern MVCCSnapshot GetSnapshotData(MVCCSnapshot snapshot);
 
 extern bool ProcArrayInstallImportedXmin(TransactionId xmin,
 										 VirtualTransactionId *sourcevxid);
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index b663d3bbc8c..5d85526b4c9 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -49,7 +49,7 @@ extern PGDLLIMPORT SnapshotData SnapshotToastData;
  */
 #define InitNonVacuumableSnapshot(snapshotdata, vistestp)  \
 	((snapshotdata).snapshot_type = SNAPSHOT_NON_VACUUMABLE, \
-	 (snapshotdata).vistest = (vistestp))
+	 (snapshotdata).nonvacuumable.vistest = (vistestp))
 
 /* This macro encodes the knowledge of which snapshots are MVCC-safe */
 #define IsMVCCSnapshot(snapshot)  \
@@ -92,7 +92,7 @@ extern void WaitForOlderSnapshots(TransactionId limitXmin, bool progress);
 extern bool ThereAreNoPriorRegisteredSnapshots(void);
 extern bool HaveRegisteredOrActiveSnapshot(void);
 
-extern char *ExportSnapshot(Snapshot snapshot);
+extern char *ExportSnapshot(MVCCSnapshot snapshot);
 
 /*
  * These live in procarray.c because they're intimately linked to the
@@ -108,19 +108,19 @@ extern bool GlobalVisCheckRemovableFullXid(Relation rel, FullTransactionId fxid)
 /*
  * Utility functions for implementing visibility routines in table AMs.
  */
-extern bool XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot);
+extern bool XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot);
 
 /* Support for catalog timetravel for logical decoding */
 struct HTAB;
 extern struct HTAB *HistoricSnapshotGetTupleCids(void);
-extern void SetupHistoricSnapshot(Snapshot historic_snapshot, struct HTAB *tuplecids);
+extern void SetupHistoricSnapshot(HistoricMVCCSnapshot historic_snapshot, struct HTAB *tuplecids);
 extern void TeardownHistoricSnapshot(bool is_error);
 extern bool HistoricSnapshotActive(void);
 
-extern Size EstimateSnapshotSpace(Snapshot snapshot);
-extern void SerializeSnapshot(Snapshot snapshot, char *start_address);
-extern Snapshot RestoreSnapshot(char *start_address);
+extern Size EstimateSnapshotSpace(MVCCSnapshot snapshot);
+extern void SerializeSnapshot(MVCCSnapshot snapshot, char *start_address);
+extern MVCCSnapshot RestoreSnapshot(char *start_address);
 struct PGPROC;
-extern void RestoreTransactionSnapshot(Snapshot snapshot, struct PGPROC *source_pgproc);
+extern void RestoreTransactionSnapshot(MVCCSnapshot snapshot, struct PGPROC *source_pgproc);
 
 #endif							/* SNAPMGR_H */
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 0e546ec1497..93c1f51784f 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -17,7 +17,7 @@
 
 
 /*
- * The different snapshot types.  We use SnapshotData structures to represent
+ * The different snapshot types.  We use the SnapshotData union to represent
  * both "regular" (MVCC) snapshots and "special" snapshots that have non-MVCC
  * semantics.  The specific semantics of a snapshot are encoded by its type.
  *
@@ -27,6 +27,9 @@
  * The reason the snapshot type rather than a callback as it used to be is
  * that that allows to use the same snapshot for different table AMs without
  * having one callback per AM.
+ *
+ * The executor deals with MVCC snapshots, but the table AM and some other
+ * parts of the system also support the special snapshots.
  */
 typedef enum SnapshotType
 {
@@ -100,7 +103,9 @@ typedef enum SnapshotType
 	/*
 	 * A tuple is visible iff it follows the rules of SNAPSHOT_MVCC, but
 	 * supports being called in timetravel context (for decoding catalog
-	 * contents in the context of logical decoding).
+	 * contents in the context of logical decoding).  A historic MVCC snapshot
+	 * should only be used on catalog tables, as we only track XIDs that
+	 * modify catalogs during logical decoding.
 	 */
 	SNAPSHOT_HISTORIC_MVCC,
 
@@ -114,37 +119,18 @@ typedef enum SnapshotType
 	SNAPSHOT_NON_VACUUMABLE,
 } SnapshotType;
 
-typedef struct SnapshotData *Snapshot;
-
-#define InvalidSnapshot		((Snapshot) NULL)
-
 /*
- * Struct representing all kind of possible snapshots.
+ * Struct representing a normal MVCC snapshot.
  *
- * There are several different kinds of snapshots:
- * * Normal MVCC snapshots
- * * MVCC snapshots taken during recovery (in Hot-Standby mode)
- * * Historic MVCC snapshots used during logical decoding
- * * snapshots passed to HeapTupleSatisfiesDirty()
- * * snapshots passed to HeapTupleSatisfiesNonVacuumable()
- * * snapshots used for SatisfiesAny, Toast, Self where no members are
- *	 accessed.
- *
- * TODO: It's probably a good idea to split this struct using a NodeTag
- * similar to how parser and executor nodes are handled, with one type for
- * each different kind of snapshot to avoid overloading the meaning of
- * individual fields.
+ * MVCC snapshots come in two variants: those taken during recovery in hot
+ * standby mode, and "normal" MVCC snapshots.  They are distinguished by
+ * takenDuringRecovery.
  */
-typedef struct SnapshotData
+typedef struct MVCCSnapshotData
 {
-	SnapshotType snapshot_type; /* type of snapshot */
+	SnapshotType snapshot_type; /* type of snapshot, must be first */
 
 	/*
-	 * The remaining fields are used only for MVCC snapshots, and are normally
-	 * just zeroes in special snapshots.  (But xmin and xmax are used
-	 * specially by HeapTupleSatisfiesDirty, and xmin is used specially by
-	 * HeapTupleSatisfiesNonVacuumable.)
-	 *
 	 * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see
 	 * the effects of all older XIDs except those listed in the snapshot. xmin
 	 * is stored as an optimization to avoid needing to search the XID arrays
@@ -154,10 +140,8 @@ typedef struct SnapshotData
 	TransactionId xmax;			/* all XID >= xmax are invisible to me */
 
 	/*
-	 * For normal MVCC snapshot this contains the all xact IDs that are in
-	 * progress, unless the snapshot was taken during recovery in which case
-	 * it's empty. For historic MVCC snapshots, the meaning is inverted, i.e.
-	 * it contains *committed* transactions between xmin and xmax.
+	 * xip contains the all xact IDs that are in progress, unless the snapshot
+	 * was taken during recovery in which case it's empty.
 	 *
 	 * note: all ids in xip[] satisfy xmin <= xip[i] < xmax
 	 */
@@ -165,10 +149,8 @@ typedef struct SnapshotData
 	uint32		xcnt;			/* # of xact ids in xip[] */
 
 	/*
-	 * For non-historic MVCC snapshots, this contains subxact IDs that are in
-	 * progress (and other transactions that are in progress if taken during
-	 * recovery). For historic snapshot it contains *all* xids assigned to the
-	 * replayed transaction, including the toplevel xid.
+	 * subxip contains subxact IDs that are in progress (and other
+	 * transactions that are in progress if taken during recovery).
 	 *
 	 * note: all ids in subxip[] are >= xmin, but we don't bother filtering
 	 * out any that are >= xmax
@@ -182,18 +164,6 @@ typedef struct SnapshotData
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-	/*
-	 * An extra return value for HeapTupleSatisfiesDirty, not used in MVCC
-	 * snapshots.
-	 */
-	uint32		speculativeToken;
-
-	/*
-	 * For SNAPSHOT_NON_VACUUMABLE (and hopefully more in the future) this is
-	 * used to determine whether row could be vacuumed.
-	 */
-	struct GlobalVisState *vistest;
-
 	/*
 	 * Book-keeping information, used by the snapshot manager
 	 */
@@ -207,6 +177,97 @@ typedef struct SnapshotData
 	 * transactions completed since the last GetSnapshotData().
 	 */
 	uint64		snapXactCompletionCount;
+} MVCCSnapshotData;
+
+typedef struct MVCCSnapshotData *MVCCSnapshot;
+
+#define InvalidMVCCSnapshot ((MVCCSnapshot) NULL)
+
+/*
+ * Struct representing a "historic" MVCC snapshot during logical decoding.
+ * These are constructed by src/replication/logical/snapbuild.c.
+ */
+typedef struct HistoricMVCCSnapshotData
+{
+	SnapshotType snapshot_type; /* type of snapshot, must be first */
+
+	/*
+	 * xmin and xmax like in a normal MVCC snapshot.
+	 */
+	TransactionId xmin;			/* all XID < xmin are visible to me */
+	TransactionId xmax;			/* all XID >= xmax are invisible to me */
+
+	/*
+	 * committed_xids contains *committed* transactions between xmin and xmax.
+	 * (This is the inverse of 'xip' in normal MVCC snapshots, which contains
+	 * all non-committed transactions.)  The array is sorted by XID to allow
+	 * binary search.
+	 *
+	 * note: all ids in committed_xids[] satisfy xmin <= committed_xids[i] <
+	 * xmax
+	 */
+	TransactionId *committed_xids;
+	uint32		xcnt;			/* # of xact ids in committed_xids[] */
+
+	/*
+	 * curxip contains *all* xids assigned to the replayed transaction,
+	 * including the toplevel xid.
+	 */
+	TransactionId *curxip;
+	int32		curxcnt;		/* # of xact ids in curxip[] */
+
+	CommandId	curcid;			/* in my xact, CID < curcid are visible */
+
+	bool		copied;			/* false if it's a "base" snapshot */
+
+	uint32		refcount;		/* refcount managed by snapbuild.c  */
+	uint32		regd_count;		/* refcount registered with resource owners */
+
+} HistoricMVCCSnapshotData;
+
+typedef struct HistoricMVCCSnapshotData *HistoricMVCCSnapshot;
+
+/*
+ * Struct representing a special "snapshot" which sees all tuples as visible
+ * if they are visible to anyone, i.e. if they are not vacuumable.
+ * i.e. SNAPSHOT_NON_VACUUMABLE.
+ */
+typedef struct NonVacuumableSnapshotData
+{
+	SnapshotType snapshot_type; /* type of snapshot, must be first */
+
+	/* This is used to determine whether row could be vacuumed. */
+	struct GlobalVisState *vistest;
+} NonVacuumableSnapshotData;
+
+/*
+ * Return values to the caller of HeapTupleSatisfyDirty.
+ */
+typedef struct DirtySnapshotData
+{
+	SnapshotType snapshot_type; /* type of snapshot, must be first */
+
+	TransactionId xmin;
+	TransactionId xmax;
+	uint32		speculativeToken;
+} DirtySnapshotData;
+
+/*
+ * Generic union representing all kind of possible snapshots.  Some have
+ * type-specific structs.
+ */
+typedef union SnapshotData
+{
+	SnapshotType snapshot_type; /* type of snapshot */
+
+	MVCCSnapshotData mvcc;
+	DirtySnapshotData dirty;
+	HistoricMVCCSnapshotData historic_mvcc;
+	NonVacuumableSnapshotData nonvacuumable;
 } SnapshotData;
 
+typedef union SnapshotData *Snapshot;
+
+#define InvalidSnapshot		((Snapshot) NULL)
+
 #endif							/* SNAPSHOT_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 04845d5e680..8f26256464f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -656,6 +656,7 @@ DictThesaurus
 DimensionInfo
 DirectoryMethodData
 DirectoryMethodFile
+DirtySnapshotData
 DisableTimeoutParams
 DiscardMode
 DiscardStmt
@@ -1217,6 +1218,7 @@ HeapTupleFreeze
 HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
+HistoricMVCCSnapshotData
 HistControl
 HotStandbyState
 I32
@@ -1673,6 +1675,7 @@ MINIDUMPWRITEDUMP
 MINIDUMP_TYPE
 MJEvalResult
 MTTargetRelLookup
+MVCCSnapshotData
 MVDependencies
 MVDependency
 MVNDistinct
@@ -1779,6 +1782,7 @@ Node
 NodeTag
 NonEmptyRange
 NoneCompressorState
+NonVacuumableSnapshotData
 Notification
 NotificationList
 NotifyStmt
-- 
2.47.3



  [text/x-patch] 0003-Simplify-historic-snapshot-refcounting.patch (13.1K, ../../[email protected]/4-0003-Simplify-historic-snapshot-refcounting.patch)
  download | inline diff:
From 904bf5e1279a9218538416d62ec71a1a34d4a6e9 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 13 Mar 2025 16:45:12 +0200
Subject: [PATCH 3/9] Simplify historic snapshot refcounting

ReorderBufferProcessTXN() handled "copied" snapshots created with
ReorderBufferCopySnap() differently from "base" historic snapshots
created by snapbuild.c. The base snapshots used a reference count,
while copied snapshots did not. Simplify by using the reference count
for both.
---
 .../replication/logical/reorderbuffer.c       | 97 ++++++++-----------
 src/backend/replication/logical/snapbuild.c   | 48 +--------
 src/include/replication/snapbuild.h           |  1 +
 src/include/utils/snapshot.h                  |  2 -
 4 files changed, 46 insertions(+), 102 deletions(-)

diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4d522787129..b7c04e5067a 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -103,7 +103,7 @@
 #include "replication/logical.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"	/* just for SnapBuildSnapDecRefcount */
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/procarray.h"
@@ -280,7 +280,6 @@ static void ReorderBufferSerializedPath(char *path, ReplicationSlot *slot,
 										TransactionId xid, XLogSegNo segno);
 static int	ReorderBufferTXNSizeCompare(const pairingheap_node *a, const pairingheap_node *b, void *arg);
 
-static void ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap);
 static HistoricMVCCSnapshot ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 												  ReorderBufferTXN *txn, CommandId cid);
 
@@ -562,7 +561,7 @@ ReorderBufferFreeChange(ReorderBuffer *rb, ReorderBufferChange *change,
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			if (change->data.snapshot)
 			{
-				ReorderBufferFreeSnap(rb, change->data.snapshot);
+				SnapBuildSnapDecRefcount(change->data.snapshot);
 				change->data.snapshot = NULL;
 			}
 			break;
@@ -1612,7 +1611,8 @@ ReorderBufferCleanupTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	if (txn->snapshot_now != NULL)
 	{
 		Assert(rbtxn_is_streamed(txn));
-		ReorderBufferFreeSnap(rb, txn->snapshot_now);
+		SnapBuildSnapDecRefcount(txn->snapshot_now);
+		txn->snapshot_now = NULL;
 	}
 
 	/*
@@ -1921,7 +1921,6 @@ ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 	snap = MemoryContextAllocZero(rb->context, size);
 	memcpy(snap, orig_snap, sizeof(HistoricMVCCSnapshotData));
 
-	snap->copied = true;
 	snap->refcount = 1;			/* mark as active so nobody frees it */
 	snap->regd_count = 0;
 	snap->committed_xids = (TransactionId *) (snap + 1);
@@ -1961,18 +1960,6 @@ ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 	return snap;
 }
 
-/*
- * Free a previously ReorderBufferCopySnap'ed snapshot
- */
-static void
-ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap)
-{
-	if (snap->copied)
-		pfree(snap);
-	else
-		SnapBuildSnapDecRefcount(snap);
-}
-
 /*
  * If the transaction was (partially) streamed, we need to prepare or commit
  * it in a 'streamed' way.  That is, we first stream the remaining part of the
@@ -2123,11 +2110,8 @@ ReorderBufferSaveTXNSnapshot(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	txn->command_id = command_id;
 
 	/* Avoid copying if it's already copied. */
-	if (snapshot_now->copied)
-		txn->snapshot_now = snapshot_now;
-	else
-		txn->snapshot_now = ReorderBufferCopySnap(rb, snapshot_now,
-												  txn, command_id);
+	txn->snapshot_now = snapshot_now;
+	SnapBuildSnapIncRefcount(txn->snapshot_now);
 }
 
 /*
@@ -2228,6 +2212,8 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 	/* setup the initial snapshot */
 	SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
+	/* increase refcount for the installed historic snapshot */
+	SnapBuildSnapIncRefcount(snapshot_now);
 
 	/*
 	 * Decoding needs access to syscaches et al., which in turn use
@@ -2531,33 +2517,12 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 					/* get rid of the old */
 					TeardownHistoricSnapshot(false);
-
-					if (snapshot_now->copied)
-					{
-						ReorderBufferFreeSnap(rb, snapshot_now);
-						snapshot_now =
-							ReorderBufferCopySnap(rb, change->data.snapshot,
-												  txn, command_id);
-					}
-
-					/*
-					 * Restored from disk, need to be careful not to double
-					 * free. We could introduce refcounting for that, but for
-					 * now this seems infrequent enough not to care.
-					 */
-					else if (change->data.snapshot->copied)
-					{
-						snapshot_now =
-							ReorderBufferCopySnap(rb, change->data.snapshot,
-												  txn, command_id);
-					}
-					else
-					{
-						snapshot_now = change->data.snapshot;
-					}
+					SnapBuildSnapDecRefcount(snapshot_now);
 
 					/* and continue with the new one */
+					snapshot_now = change->data.snapshot;
 					SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
+					SnapBuildSnapIncRefcount(snapshot_now);
 					break;
 
 				case REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID:
@@ -2567,16 +2532,26 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 					{
 						command_id = change->data.command_id;
 
-						if (!snapshot_now->copied)
+						TeardownHistoricSnapshot(false);
+
+						/*
+						 * Construct a new snapshot with the new command ID.
+						 *
+						 * If this is the only reference to the snapshot, and
+						 * it's a "copied" snapshot that already contains all
+						 * the replayed transaction's XIDs (curxnct > 0), we
+						 * can take a shortcut and update the snapshot's
+						 * command ID in place.
+						 */
+						if (snapshot_now->refcount == 1 && snapshot_now->curxcnt > 0)
+							snapshot_now->curcid = command_id;
+						else
 						{
-							/* we don't use the global one anymore */
+							SnapBuildSnapDecRefcount(snapshot_now);
 							snapshot_now = ReorderBufferCopySnap(rb, snapshot_now,
 																 txn, command_id);
 						}
 
-						snapshot_now->curcid = command_id;
-
-						TeardownHistoricSnapshot(false);
 						SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
 					}
 
@@ -2666,11 +2641,11 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		 */
 		if (streaming)
 			ReorderBufferSaveTXNSnapshot(rb, txn, snapshot_now, command_id);
-		else if (snapshot_now->copied)
-			ReorderBufferFreeSnap(rb, snapshot_now);
 
 		/* cleanup */
 		TeardownHistoricSnapshot(false);
+		SnapBuildSnapDecRefcount(snapshot_now);
+		snapshot_now = NULL;
 
 		/*
 		 * Aborting the current (sub-)transaction as a whole has the right
@@ -2737,6 +2712,11 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		TeardownHistoricSnapshot(true);
 
+		/*
+		 * don't decrement the refcount on snapshot_now yet, we still use it
+		 * in the ReorderBufferResetTXN() call below.
+		 */
+
 		/*
 		 * Force cache invalidation to happen outside of a valid transaction
 		 * to prevent catalog access as we just caught an error.
@@ -2798,9 +2778,15 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			ReorderBufferResetTXN(rb, txn, snapshot_now,
 								  command_id, prev_lsn,
 								  specinsert);
+
+			SnapBuildSnapDecRefcount(snapshot_now);
+			snapshot_now = NULL;
 		}
 		else
 		{
+			SnapBuildSnapDecRefcount(snapshot_now);
+			snapshot_now = NULL;
+
 			ReorderBufferCleanupTXN(rb, txn);
 			MemoryContextSwitchTo(ecxt);
 			PG_RE_THROW();
@@ -4420,8 +4406,7 @@ ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 											 txn, command_id);
 
 		/* Free the previously copied snapshot. */
-		Assert(txn->snapshot_now->copied);
-		ReorderBufferFreeSnap(rb, txn->snapshot_now);
+		SnapBuildSnapDecRefcount(txn->snapshot_now);
 		txn->snapshot_now = NULL;
 	}
 
@@ -4811,7 +4796,7 @@ ReorderBufferRestoreChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				newsnap->committed_xids = (TransactionId *)
 					(((char *) newsnap) + sizeof(HistoricMVCCSnapshotData));
 				newsnap->curxip = newsnap->committed_xids + newsnap->xcnt;
-				newsnap->copied = true;
+				newsnap->refcount = 1;
 				break;
 			}
 			/* the base struct contains all the data, easy peasy */
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 80f4b620520..2e8ea9e21dd 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -157,10 +157,6 @@ static void SnapBuildPurgeOlderTxn(SnapBuild *builder);
 /* snapshot building/manipulation/distribution functions */
 static HistoricMVCCSnapshot SnapBuildBuildSnapshot(SnapBuild *builder);
 
-static void SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap);
-
-static void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
-
 static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
 
 static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
@@ -245,29 +241,6 @@ FreeSnapshotBuilder(SnapBuild *builder)
 	MemoryContextDelete(context);
 }
 
-/*
- * Free an unreferenced snapshot that has previously been built by us.
- */
-static void
-SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap)
-{
-	/* make sure we don't get passed an external snapshot */
-	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
-
-	/* make sure nobody modified our snapshot */
-	Assert(snap->curcid == FirstCommandId);
-	Assert(snap->regd_count == 0);
-
-	/* slightly more likely, so it's checked even without c-asserts */
-	if (snap->copied)
-		elog(ERROR, "cannot free a copied snapshot");
-
-	if (snap->refcount)
-		elog(ERROR, "cannot free a snapshot that's in use");
-
-	pfree(snap);
-}
-
 /*
  * In which state of snapshot building are we?
  */
@@ -310,7 +283,7 @@ SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr)
  * This is used when handing out a snapshot to some external resource or when
  * adding a Snapshot as builder->snapshot.
  */
-static void
+void
 SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 {
 	snap->refcount++;
@@ -318,9 +291,6 @@ SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 
 /*
  * Decrease refcount of a snapshot and free if the refcount reaches zero.
- *
- * Externally visible, so that external resources that have been handed an
- * IncRef'ed Snapshot can adjust its refcount easily.
  */
 void
 SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
@@ -328,19 +298,12 @@ SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
 	/* make sure we don't get passed an external snapshot */
 	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
-	/* make sure nobody modified our snapshot */
-	Assert(snap->curcid == FirstCommandId);
-
 	Assert(snap->refcount > 0);
 	Assert(snap->regd_count == 0);
 
-	/* slightly more likely, so it's checked even without casserts */
-	if (snap->copied)
-		elog(ERROR, "cannot free a copied snapshot");
-
 	snap->refcount--;
 	if (snap->refcount == 0)
-		SnapBuildFreeSnapshot(snap);
+		pfree(snap);
 }
 
 /*
@@ -413,7 +376,6 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
 	snapshot->curxcnt = 0;
 	snapshot->curxip = NULL;
 
-	snapshot->copied = false;
 	snapshot->curcid = FirstCommandId;
 	snapshot->refcount = 0;
 	snapshot->regd_count = 0;
@@ -1082,18 +1044,16 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
 			SnapBuildSnapDecRefcount(builder->snapshot);
 
 		builder->snapshot = SnapBuildBuildSnapshot(builder);
+		SnapBuildSnapIncRefcount(builder->snapshot);
 
 		/* we might need to execute invalidations, add snapshot */
 		if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, xid))
 		{
-			SnapBuildSnapIncRefcount(builder->snapshot);
 			ReorderBufferSetBaseSnapshot(builder->reorder, xid, lsn,
 										 builder->snapshot);
+			SnapBuildSnapIncRefcount(builder->snapshot);
 		}
 
-		/* refcount of the snapshot builder for the new snapshot */
-		SnapBuildSnapIncRefcount(builder->snapshot);
-
 		/*
 		 * Add a new catalog snapshot and invalidations messages to all
 		 * currently running transactions.
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 5930ffb55a8..6095013a299 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -70,6 +70,7 @@ extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  XLogRecPtr two_phase_at);
 extern void FreeSnapshotBuilder(SnapBuild *builder);
 
+extern void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
 extern void SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap);
 
 extern MVCCSnapshot SnapBuildInitialSnapshot(SnapBuild *builder);
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 93c1f51784f..bca0ad16e68 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -218,8 +218,6 @@ typedef struct HistoricMVCCSnapshotData
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-	bool		copied;			/* false if it's a "base" snapshot */
-
 	uint32		refcount;		/* refcount managed by snapbuild.c  */
 	uint32		regd_count;		/* refcount registered with resource owners */
 
-- 
2.47.3



  [text/x-patch] 0004-Add-an-explicit-valid-flag-to-MVCCSnapshotData.patch (4.0K, ../../[email protected]/5-0004-Add-an-explicit-valid-flag-to-MVCCSnapshotData.patch)
  download | inline diff:
From 6f49e70cd926f0a5906636e8e82253598bff561f Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 23:47:48 +0300
Subject: [PATCH 4/9] Add an explicit 'valid' flag to MVCCSnapshotData

The lifetime of the "static" snapshots returned by
GetTransactionSnapshot(), GetLatestSnapshot() and GetCatalogSnapshot()
is a bit vague. By adding an explicit 'valid' flag, we can make it
more clear when a function call updates a static snapshot, making it
valid, and when another function makes it invalid again. It's
currently only used in assertions, and can also be handy when
debugging.
---
 src/backend/storage/ipc/procarray.c |  2 ++
 src/backend/utils/time/snapmgr.c    | 15 +++++++++++++++
 src/include/utils/snapshot.h        |  1 +
 3 files changed, 18 insertions(+)

diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 2292d8a09af..29a743ee49c 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2083,6 +2083,7 @@ GetSnapshotDataReuse(MVCCSnapshot snapshot)
 	snapshot->active_count = 0;
 	snapshot->regd_count = 0;
 	snapshot->copied = false;
+	snapshot->valid = true;
 
 	return true;
 }
@@ -2462,6 +2463,7 @@ GetSnapshotData(MVCCSnapshot snapshot)
 	snapshot->active_count = 0;
 	snapshot->regd_count = 0;
 	snapshot->copied = false;
+	snapshot->valid = true;
 
 	return snapshot;
 }
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 85395f0a8df..455afc9bf98 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -459,6 +459,7 @@ InvalidateCatalogSnapshot(void)
 	{
 		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
 		CatalogSnapshot = NULL;
+		CatalogSnapshotData.valid = false;
 		SnapshotResetXmin();
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
 	}
@@ -624,6 +625,7 @@ CopyMVCCSnapshot(MVCCSnapshot snapshot)
 	newsnap->regd_count = 0;
 	newsnap->active_count = 0;
 	newsnap->copied = true;
+	newsnap->valid = true;
 	newsnap->snapXactCompletionCount = 0;
 
 	/* setup XID array */
@@ -665,6 +667,7 @@ FreeMVCCSnapshot(MVCCSnapshot snapshot)
 	Assert(snapshot->regd_count == 0);
 	Assert(snapshot->active_count == 0);
 	Assert(snapshot->copied);
+	Assert(snapshot->valid);
 
 	pfree(snapshot);
 }
@@ -701,6 +704,7 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 
 	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
 	origsnap = &snapshot->mvcc;
+	Assert(origsnap->valid);
 
 	Assert(ActiveSnapshot == NULL || snap_level >= ActiveSnapshot->as_level);
 
@@ -860,6 +864,7 @@ RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 
 	Assert(orig_snapshot->snapshot_type == SNAPSHOT_MVCC);
 	snapshot = &orig_snapshot->mvcc;
+	Assert(snapshot->valid);
 
 	/* Static snapshot?  Create a persistent copy */
 	snapshot = snapshot->copied ? snapshot : CopyMVCCSnapshot(snapshot);
@@ -981,6 +986,15 @@ SnapshotResetXmin(void)
 {
 	MVCCSnapshot minSnapshot;
 
+	/*
+	 * These static snapshots are not in the RegisteredSnapshots list, so we
+	 * might advance MyProc->xmin past their xmin. (Note that in case of
+	 * IsolationUsesXactSnapshot() == true, CurrentSnapshot points to the copy
+	 * in FirstSnapshot rather than CurrentSnapshotData.)
+	 */
+	CurrentSnapshotData.valid = false;
+	SecondarySnapshotData.valid = false;
+
 	if (ActiveSnapshot != NULL)
 		return;
 
@@ -1884,6 +1898,7 @@ RestoreSnapshot(char *start_address)
 	snapshot->regd_count = 0;
 	snapshot->active_count = 0;
 	snapshot->copied = true;
+	snapshot->valid = true;
 
 	return snapshot;
 }
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index bca0ad16e68..1697c6df856 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -161,6 +161,7 @@ typedef struct MVCCSnapshotData
 
 	bool		takenDuringRecovery;	/* recovery-shaped snapshot? */
 	bool		copied;			/* false if it's a static snapshot */
+	bool		valid;			/* is this snapshot valid? */
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-- 
2.47.3



  [text/x-patch] 0005-Replace-static-snapshot-pointers-with-the-valid-flag.patch (14.5K, ../../[email protected]/6-0005-Replace-static-snapshot-pointers-with-the-valid-flag.patch)
  download | inline diff:
From b060c1f30032d1cb18e2795be09412e3b84722cf Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 21:44:43 +0300
Subject: [PATCH 5/9] Replace static snapshot pointers with the 'valid' flags

Previously, we used the pointers like SecondarySnapshot and
CatalogSnapshot to indicate whether the corresponding static snapshot
is valid or not, but now that we have an explicit flag in
MVCCSnapshotData for that, we replace checks like "SecondarySnapshot
!= NULL" with "SecondarySnapshotData.valid", and get rid of the
separate pointer variables.

The situation with CurrentSnapshot was a bit more
complicated. Usually, it pointed to CurrentSnapshotData, but could
also point to the palloc'd FirstXactSnapshot. This gets rid of the
palloc'd FirstXactSnapshot, and instead we just refrain from modifying
CurrentSnapshotData when in a serializable transaction.
---
 src/backend/utils/time/snapmgr.c | 162 ++++++++++++++-----------------
 1 file changed, 75 insertions(+), 87 deletions(-)

diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 455afc9bf98..0ab81736742 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -67,8 +67,8 @@
  * In addition to snapshots pushed to the active snapshot stack, a snapshot
  * can be registered with a resource owner.
  *
- * The FirstXactSnapshot, if any, is treated a bit specially: we increment its
- * regd_count and list it in RegisteredSnapshots, but this reference is not
+ * If FirstXactSnapshotRegistered is set, we increment the static
+ * CurrentSnapshotData's regd_count and list it in RegisteredSnapshots, but this reference is not
  * tracked by a resource owner. We used to use the TopTransactionResourceOwner
  * to track this snapshot reference, but that introduces logical circularity
  * and thus makes it impossible to clean up in a sane fashion.  It's better to
@@ -146,9 +146,6 @@ SnapshotData SnapshotAnyData = {SNAPSHOT_ANY};
 SnapshotData SnapshotToastData = {SNAPSHOT_TOAST};
 
 /* Pointers to valid snapshots */
-static MVCCSnapshot CurrentSnapshot = NULL;
-static MVCCSnapshot SecondarySnapshot = NULL;
-static MVCCSnapshot CatalogSnapshot = NULL;
 static HistoricMVCCSnapshot HistoricSnapshot = NULL;
 
 /*
@@ -193,11 +190,12 @@ static pairingheap RegisteredSnapshots = {&xmin_cmp, NULL, NULL};
 bool		FirstSnapshotSet = false;
 
 /*
- * Remember the serializable transaction snapshot, if any.  We cannot trust
- * FirstSnapshotSet in combination with IsolationUsesXactSnapshot(), because
- * GUC may be reset before us, changing the value of IsolationUsesXactSnapshot.
+ * If set, CurrentSnapshotData is valid and is tracked internally.  We cannot
+ * trust FirstSnapshotSet in combination with IsolationUsesXactSnapshot(),
+ * because GUC may be reset before us, changing the value of
+ * IsolationUsesXactSnapshot.
  */
-static MVCCSnapshot FirstXactSnapshot = NULL;
+static bool FirstXactSnapshotRegistered = false;
 
 /* Define pathname of exported-snapshot files */
 #define SNAPSHOT_EXPORT_DIR "pg_snapshots"
@@ -300,7 +298,7 @@ GetTransactionSnapshot(void)
 		InvalidateCatalogSnapshot();
 
 		Assert(pairingheap_is_empty(&RegisteredSnapshots));
-		Assert(FirstXactSnapshot == NULL);
+		Assert(!FirstXactSnapshotRegistered);
 
 		if (IsInParallelMode())
 			elog(ERROR,
@@ -308,42 +306,44 @@ GetTransactionSnapshot(void)
 
 		/*
 		 * In transaction-snapshot mode, the first snapshot must live until
-		 * end of xact regardless of what the caller does with it, so we must
-		 * make a copy of it rather than returning CurrentSnapshotData
-		 * directly.  Furthermore, if we're running in serializable mode,
-		 * predicate.c needs to wrap the snapshot fetch in its own processing.
+		 * end of xact regardless of what the caller does with it, so we keep
+		 * it in RegisteredSnapshots even though it's not tracked by any
+		 * resource owner.  Furthermore, if we're running in serializable
+		 * mode, predicate.c needs to wrap the snapshot fetch in its own
+		 * processing.
 		 */
 		if (IsolationUsesXactSnapshot())
 		{
 			/* First, create the snapshot in CurrentSnapshotData */
 			if (IsolationIsSerializable())
-				CurrentSnapshot = GetSerializableTransactionSnapshot(&CurrentSnapshotData);
+				GetSerializableTransactionSnapshot(&CurrentSnapshotData);
 			else
-				CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
-
-			/* Make a saved copy */
-			CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
-			FirstXactSnapshot = CurrentSnapshot;
-			/* Mark it as "registered" in FirstXactSnapshot */
-			FirstXactSnapshot->regd_count++;
-			pairingheap_add(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+				GetSnapshotData(&CurrentSnapshotData);
+
+			/* Mark it as "registered" */
+			CurrentSnapshotData.regd_count++;
+			FirstXactSnapshotRegistered = true;
+			pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 		}
 		else
-			CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+			GetSnapshotData(&CurrentSnapshotData);
 
 		FirstSnapshotSet = true;
-		return (Snapshot) CurrentSnapshot;
+		return (Snapshot) &CurrentSnapshotData;
 	}
 
 	if (IsolationUsesXactSnapshot())
-		return (Snapshot) CurrentSnapshot;
+	{
+		Assert(CurrentSnapshotData.valid);
+		return (Snapshot) &CurrentSnapshotData;
+	}
 
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
-	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+	GetSnapshotData(&CurrentSnapshotData);
 
-	return (Snapshot) CurrentSnapshot;
+	return (Snapshot) &CurrentSnapshotData;
 }
 
 /*
@@ -372,9 +372,9 @@ GetLatestSnapshot(void)
 	if (!FirstSnapshotSet)
 		return GetTransactionSnapshot();
 
-	SecondarySnapshot = GetSnapshotData(&SecondarySnapshotData);
+	GetSnapshotData(&SecondarySnapshotData);
 
-	return (Snapshot) SecondarySnapshot;
+	return (Snapshot) &SecondarySnapshotData;
 }
 
 /*
@@ -414,15 +414,15 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 	 * scan a relation for which neither catcache nor snapshot invalidations
 	 * are sent, we must refresh the snapshot every time.
 	 */
-	if (CatalogSnapshot &&
+	if (CatalogSnapshotData.valid &&
 		!RelationInvalidatesSnapshotsOnly(relid) &&
 		!RelationHasSysCache(relid))
 		InvalidateCatalogSnapshot();
 
-	if (CatalogSnapshot == NULL)
+	if (!CatalogSnapshotData.valid)
 	{
 		/* Get new snapshot. */
-		CatalogSnapshot = GetSnapshotData(&CatalogSnapshotData);
+		GetSnapshotData(&CatalogSnapshotData);
 
 		/*
 		 * Make sure the catalog snapshot will be accounted for in decisions
@@ -436,10 +436,10 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		 * NB: it had better be impossible for this to throw error, since the
 		 * CatalogSnapshot pointer is already valid.
 		 */
-		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
+		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
 	}
 
-	return (Snapshot) CatalogSnapshot;
+	return (Snapshot) &CatalogSnapshotData;
 }
 
 /*
@@ -455,10 +455,9 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 void
 InvalidateCatalogSnapshot(void)
 {
-	if (CatalogSnapshot)
+	if (CatalogSnapshotData.valid)
 	{
-		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
-		CatalogSnapshot = NULL;
+		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
 		CatalogSnapshotData.valid = false;
 		SnapshotResetXmin();
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
@@ -478,7 +477,7 @@ InvalidateCatalogSnapshot(void)
 void
 InvalidateCatalogSnapshotConditionally(void)
 {
-	if (CatalogSnapshot &&
+	if (CatalogSnapshotData.valid &&
 		ActiveSnapshot == NULL &&
 		pairingheap_is_singular(&RegisteredSnapshots))
 		InvalidateCatalogSnapshot();
@@ -494,10 +493,10 @@ SnapshotSetCommandId(CommandId curcid)
 	if (!FirstSnapshotSet)
 		return;
 
-	if (CurrentSnapshot)
-		CurrentSnapshot->curcid = curcid;
-	if (SecondarySnapshot)
-		SecondarySnapshot->curcid = curcid;
+	if (CurrentSnapshotData.valid)
+		CurrentSnapshotData.curcid = curcid;
+	if (SecondarySnapshotData.valid)
+		SecondarySnapshotData.curcid = curcid;
 	/* Should we do the same with CatalogSnapshot? */
 }
 
@@ -520,7 +519,7 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	InvalidateCatalogSnapshot();
 
 	Assert(pairingheap_is_empty(&RegisteredSnapshots));
-	Assert(FirstXactSnapshot == NULL);
+	Assert(!FirstXactSnapshotRegistered);
 	Assert(!HistoricSnapshotActive());
 
 	/*
@@ -529,28 +528,28 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	 * CurrentSnapshotData's XID arrays have been allocated, and (2) to update
 	 * the state for GlobalVis*.
 	 */
-	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+	GetSnapshotData(&CurrentSnapshotData);
 
 	/*
 	 * Now copy appropriate fields from the source snapshot.
 	 */
-	CurrentSnapshot->xmin = sourcesnap->xmin;
-	CurrentSnapshot->xmax = sourcesnap->xmax;
-	CurrentSnapshot->xcnt = sourcesnap->xcnt;
+	CurrentSnapshotData.xmin = sourcesnap->xmin;
+	CurrentSnapshotData.xmax = sourcesnap->xmax;
+	CurrentSnapshotData.xcnt = sourcesnap->xcnt;
 	Assert(sourcesnap->xcnt <= GetMaxSnapshotXidCount());
 	if (sourcesnap->xcnt > 0)
-		memcpy(CurrentSnapshot->xip, sourcesnap->xip,
+		memcpy(CurrentSnapshotData.xip, sourcesnap->xip,
 			   sourcesnap->xcnt * sizeof(TransactionId));
-	CurrentSnapshot->subxcnt = sourcesnap->subxcnt;
+	CurrentSnapshotData.subxcnt = sourcesnap->subxcnt;
 	Assert(sourcesnap->subxcnt <= GetMaxSnapshotSubxidCount());
 	if (sourcesnap->subxcnt > 0)
-		memcpy(CurrentSnapshot->subxip, sourcesnap->subxip,
+		memcpy(CurrentSnapshotData.subxip, sourcesnap->subxip,
 			   sourcesnap->subxcnt * sizeof(TransactionId));
-	CurrentSnapshot->suboverflowed = sourcesnap->suboverflowed;
-	CurrentSnapshot->takenDuringRecovery = sourcesnap->takenDuringRecovery;
+	CurrentSnapshotData.suboverflowed = sourcesnap->suboverflowed;
+	CurrentSnapshotData.takenDuringRecovery = sourcesnap->takenDuringRecovery;
 	/* NB: curcid should NOT be copied, it's a local matter */
 
-	CurrentSnapshot->snapXactCompletionCount = 0;
+	CurrentSnapshotData.snapXactCompletionCount = 0;
 
 	/*
 	 * Now we have to fix what GetSnapshotData did with MyProc->xmin and
@@ -565,13 +564,13 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	 */
 	if (sourceproc != NULL)
 	{
-		if (!ProcArrayInstallRestoredXmin(CurrentSnapshot->xmin, sourceproc))
+		if (!ProcArrayInstallRestoredXmin(CurrentSnapshotData.xmin, sourceproc))
 			ereport(ERROR,
 					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 					 errmsg("could not import the requested snapshot"),
 					 errdetail("The source transaction is not running anymore.")));
 	}
-	else if (!ProcArrayInstallImportedXmin(CurrentSnapshot->xmin, sourcevxid))
+	else if (!ProcArrayInstallImportedXmin(CurrentSnapshotData.xmin, sourcevxid))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("could not import the requested snapshot"),
@@ -580,20 +579,19 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 
 	/*
 	 * In transaction-snapshot mode, the first snapshot must live until end of
-	 * xact, so we must make a copy of it.  Furthermore, if we're running in
-	 * serializable mode, predicate.c needs to do its own processing.
+	 * xact, so we include it in RegisteredSnapshots.  Furthermore, if we're
+	 * running in serializable mode, predicate.c needs to do its own
+	 * processing.
 	 */
 	if (IsolationUsesXactSnapshot())
 	{
 		if (IsolationIsSerializable())
-			SetSerializableTransactionSnapshot(CurrentSnapshot, sourcevxid,
+			SetSerializableTransactionSnapshot(&CurrentSnapshotData, sourcevxid,
 											   sourcepid);
-		/* Make a saved copy */
-		CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
-		FirstXactSnapshot = CurrentSnapshot;
-		/* Mark it as "registered" in FirstXactSnapshot */
-		FirstXactSnapshot->regd_count++;
-		pairingheap_add(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+		/* Mark it as "registered" */
+		FirstXactSnapshotRegistered = true;
+		CurrentSnapshotData.regd_count++;
+		pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 	}
 
 	FirstSnapshotSet = true;
@@ -710,15 +708,8 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 
 	newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
 
-	/*
-	 * Checking SecondarySnapshot is probably useless here, but it seems
-	 * better to be sure.
-	 */
-	if (origsnap == CurrentSnapshot || origsnap == SecondarySnapshot ||
-		!origsnap->copied)
-		newactive->as_snap = CopyMVCCSnapshot(origsnap);
-	else
-		newactive->as_snap = origsnap;
+	newactive->as_snap = origsnap->copied ?
+		origsnap : CopyMVCCSnapshot(origsnap);
 
 	newactive->as_next = ActiveSnapshot;
 	newactive->as_level = snap_level;
@@ -987,12 +978,10 @@ SnapshotResetXmin(void)
 	MVCCSnapshot minSnapshot;
 
 	/*
-	 * These static snapshots are not in the RegisteredSnapshots list, so we
-	 * might advance MyProc->xmin past their xmin. (Note that in case of
-	 * IsolationUsesXactSnapshot() == true, CurrentSnapshot points to the copy
-	 * in FirstSnapshot rather than CurrentSnapshotData.)
+	 * Invalidate these static snapshots so that we can advance xmin.
 	 */
-	CurrentSnapshotData.valid = false;
+	if (!FirstXactSnapshotRegistered)
+		CurrentSnapshotData.valid = false;
 	SecondarySnapshotData.valid = false;
 
 	if (ActiveSnapshot != NULL)
@@ -1081,13 +1070,13 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * stacked as active, we don't want the code below to be chasing through a
 	 * dangling pointer.
 	 */
-	if (FirstXactSnapshot != NULL)
+	if (FirstXactSnapshotRegistered)
 	{
-		Assert(FirstXactSnapshot->regd_count > 0);
+		Assert(CurrentSnapshotData.regd_count > 0);
 		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
-		pairingheap_remove(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+		pairingheap_remove(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
+		FirstXactSnapshotRegistered = false;
 	}
-	FirstXactSnapshot = NULL;
 
 	/*
 	 * If we exported any snapshots, clean them up.
@@ -1145,9 +1134,8 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	ActiveSnapshot = NULL;
 	pairingheap_reset(&RegisteredSnapshots);
 
-	CurrentSnapshot = NULL;
-	SecondarySnapshot = NULL;
-
+	CurrentSnapshotData.valid = false;
+	SecondarySnapshotData.valid = false;
 	FirstSnapshotSet = false;
 
 	/*
@@ -1708,7 +1696,7 @@ HaveRegisteredOrActiveSnapshot(void)
 	 * removed at any time due to invalidation processing. If explicitly
 	 * registered more than one snapshot has to be in RegisteredSnapshots.
 	 */
-	if (CatalogSnapshot != NULL &&
+	if (CatalogSnapshotData.valid &&
 		pairingheap_is_singular(&RegisteredSnapshots))
 		return false;
 
-- 
2.47.3



  [text/x-patch] 0006-WIP-Make-RestoreSnapshot-register-the-snapshot-with-.patch (3.7K, ../../[email protected]/7-0006-WIP-Make-RestoreSnapshot-register-the-snapshot-with-.patch)
  download | inline diff:
From f542826b420d41d18dc82d068f774559a4d8f649 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 19:54:39 +0300
Subject: [PATCH 6/9] WIP: Make RestoreSnapshot register the snapshot with
 current resowner

This simplifies the next commit
---
 src/backend/access/index/indexam.c    | 1 -
 src/backend/access/table/tableam.c    | 2 --
 src/backend/access/transam/parallel.c | 4 ++++
 src/backend/utils/time/snapmgr.c      | 9 +++++++--
 4 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 7ef5031d716..3828622665c 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -603,7 +603,6 @@ index_beginscan_parallel(Relation heaprel, Relation indexrel,
 	Assert(RelFileLocatorEquals(indexrel->rd_locator, pscan->ps_indexlocator));
 
 	snapshot = (Snapshot) RestoreSnapshot(pscan->ps_snapshot_data);
-	snapshot = RegisterSnapshot(snapshot);
 	scan = index_beginscan_internal(indexrel, nkeys, norderbys, snapshot,
 									pscan, true);
 
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index fde28accfd3..c8db2918f40 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -175,7 +175,6 @@ table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan)
 	{
 		/* Snapshot was serialized -- restore it */
 		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		snapshot = RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
 	else
@@ -205,7 +204,6 @@ table_beginscan_parallel_tidrange(Relation relation,
 	{
 		/* Snapshot was serialized -- restore it */
 		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
 	else
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 1fd2146358d..0bd3e65f849 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -1506,6 +1506,10 @@ ParallelWorkerMain(Datum main_arg)
 							   fps->parallel_leader_pgproc);
 	PushActiveSnapshot(asnapshot);
 
+	UnregisterSnapshot(asnapshot);
+	if (tsnapshot != asnapshot)
+		UnregisterSnapshot(tsnapshot);
+
 	/*
 	 * We've changed which tuples we can see, and must therefore invalidate
 	 * system caches.
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 0ab81736742..e9a42437828 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -518,7 +518,6 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	/* Better do this to ensure following Assert succeeds. */
 	InvalidateCatalogSnapshot();
 
-	Assert(pairingheap_is_empty(&RegisteredSnapshots));
 	Assert(!FirstXactSnapshotRegistered);
 	Assert(!HistoricSnapshotActive());
 
@@ -1831,7 +1830,7 @@ SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
  *		Restore a serialized snapshot from the specified address.
  *
  * The copy is palloc'd in TopTransactionContext and has initial refcounts set
- * to 0.  The returned snapshot has the copied flag set.
+ * to 0.  The returned snapshot is registered with the current resource owner.
  */
 MVCCSnapshot
 RestoreSnapshot(char *start_address)
@@ -1888,6 +1887,12 @@ RestoreSnapshot(char *start_address)
 	snapshot->copied = true;
 	snapshot->valid = true;
 
+	/* and tell resowner.c about it, just like RegisterSnapshot() */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	snapshot->regd_count++;
+	ResourceOwnerRememberSnapshot(CurrentResourceOwner, (Snapshot) snapshot);
+	pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
+
 	return snapshot;
 }
 
-- 
2.47.3



  [text/x-patch] 0007-WIP-Replace-the-RegisteredSnapshot-pairing-heap-with.patch (21.6K, ../../[email protected]/8-0007-WIP-Replace-the-RegisteredSnapshot-pairing-heap-with.patch)
  download | inline diff:
From 7e0d5e240e187e2af7b6bad750bd3a4884ef6ea4 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 23:50:55 +0300
Subject: [PATCH 7/9] WIP: Replace the RegisteredSnapshot pairing heap with a
 linked list

Previously, we kept all the snapshots in a pairing heap, so that we
could cheaply find the snapshot with the smallest xmin. However, we
can easily use a doubly-linked list instead, which is a little
simpler. A newly acquired snapshot's xmin is always greater than or
equal to that of any previous snapshot's, so we can simply push new
snapshots to the tail of the list, and the oldest xmin is always at
the head.

Previously, we would only push a snapshot to the heap when it's
registered or pushed to the active stack, not immediately when the
GetSnapshotData() was called. Because of that, snapshots were
sometimes added to the heap out of order. But if we update the list
earlier, after each GetSnapshotData() call, it stays in order. That
means that the list now contains *all* valid snapshots, including the
snapshots that are in the active stack, and the static CurrentSnapshot
and SecondarySnapshot, whenever they are valid. (CatalogSnapshot was
already tracked by the heap)
---
 src/backend/utils/time/snapmgr.c    | 287 +++++++++++++++++-----------
 src/include/access/spgist_private.h |   1 +
 src/include/utils/snapshot.h        |   6 +-
 3 files changed, 182 insertions(+), 112 deletions(-)

diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index e9a42437828..0ff8cf9bf5f 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -67,32 +67,22 @@
  * In addition to snapshots pushed to the active snapshot stack, a snapshot
  * can be registered with a resource owner.
  *
- * If FirstXactSnapshotRegistered is set, we increment the static
- * CurrentSnapshotData's regd_count and list it in RegisteredSnapshots, but this reference is not
- * tracked by a resource owner. We used to use the TopTransactionResourceOwner
- * to track this snapshot reference, but that introduces logical circularity
- * and thus makes it impossible to clean up in a sane fashion.  It's better to
- * handle this reference as an internally-tracked registration, so that this
- * module is entirely lower-level than ResourceOwners.
+ * Xmin tracking
+ * -------------
  *
- * Likewise, any snapshots that have been exported by pg_export_snapshot
- * have regd_count = 1 and are listed in RegisteredSnapshots, but are not
- * tracked by any resource owner.
+ * All valid snapshots, whether they are "static", included the active stack,
+ * or registered with a resource owner, are tracked in a doubly-linked list,
+ * ValidSnapshots.  Any snapshots that have been exported by
+ * pg_export_snapshot() are also listed there.  (They have regd_count = 1,
+ * even though they are not tracked by any resource owner).
  *
- * Likewise, the CatalogSnapshot is listed in RegisteredSnapshots when it
- * is valid, but is not tracked by any resource owner.
- *
- * The same is true for historic snapshots used during logical decoding,
- * their lifetime is managed separately (as they live longer than one xact.c
- * transaction).
- *
- * These arrangements let us reset MyProc->xmin when there are no snapshots
+ * The list is in xmin order, so that the tail always contains the oldest
+ * snapshot.  That let us reset MyProc->xmin when there are no snapshots
  * referenced by this transaction, and advance it when the one with oldest
- * Xmin is no longer referenced.  For simplicity however, only registered
- * snapshots not active snapshots participate in tracking which one is oldest;
- * we don't try to change MyProc->xmin except when the active-snapshot
- * stack is empty.
+ * Xmin is no longer referenced.
  *
+ * The lifetime of historic snapshots used during logical decoding is managed
+ * separately (as they live longer than one xact.c transaction).
  *
  * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -111,7 +101,6 @@
 #include "access/transam.h"
 #include "access/xact.h"
 #include "datatype/timestamp.h"
-#include "lib/pairingheap.h"
 #include "miscadmin.h"
 #include "port/pg_lfind.h"
 #include "storage/fd.h"
@@ -178,13 +167,10 @@ typedef struct ActiveSnapshotElt
 static ActiveSnapshotElt *ActiveSnapshot = NULL;
 
 /*
- * Currently registered Snapshots.  Ordered in a heap by xmin, so that we can
+ * Currently valid Snapshots.  Ordered in a heap by xmin, so that we can
  * quickly find the one with lowest xmin, to advance our MyProc->xmin.
  */
-static int	xmin_cmp(const pairingheap_node *a, const pairingheap_node *b,
-					 void *arg);
-
-static pairingheap RegisteredSnapshots = {&xmin_cmp, NULL, NULL};
+static dlist_head ValidSnapshots = DLIST_STATIC_INIT(ValidSnapshots);
 
 /* first GetTransactionSnapshot call in a transaction? */
 bool		FirstSnapshotSet = false;
@@ -215,6 +201,8 @@ static MVCCSnapshot CopyMVCCSnapshot(MVCCSnapshot snapshot);
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
 static void FreeMVCCSnapshot(MVCCSnapshot snapshot);
 static void SnapshotResetXmin(void);
+static void valid_snapshots_push_tail(MVCCSnapshot snapshot);
+static void valid_snapshots_push_out_of_order(MVCCSnapshot snapshot);
 
 /* ResourceOwner callbacks to track snapshot references */
 static void ResOwnerReleaseSnapshot(Datum res);
@@ -297,7 +285,7 @@ GetTransactionSnapshot(void)
 		 */
 		InvalidateCatalogSnapshot();
 
-		Assert(pairingheap_is_empty(&RegisteredSnapshots));
+		Assert(dlist_is_empty(&ValidSnapshots));
 		Assert(!FirstXactSnapshotRegistered);
 
 		if (IsInParallelMode())
@@ -321,12 +309,13 @@ GetTransactionSnapshot(void)
 				GetSnapshotData(&CurrentSnapshotData);
 
 			/* Mark it as "registered" */
-			CurrentSnapshotData.regd_count++;
 			FirstXactSnapshotRegistered = true;
-			pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 		}
 		else
+		{
 			GetSnapshotData(&CurrentSnapshotData);
+		}
+		valid_snapshots_push_tail(&CurrentSnapshotData);
 
 		FirstSnapshotSet = true;
 		return (Snapshot) &CurrentSnapshotData;
@@ -334,6 +323,7 @@ GetTransactionSnapshot(void)
 
 	if (IsolationUsesXactSnapshot())
 	{
+		Assert(FirstXactSnapshotRegistered);
 		Assert(CurrentSnapshotData.valid);
 		return (Snapshot) &CurrentSnapshotData;
 	}
@@ -341,7 +331,10 @@ GetTransactionSnapshot(void)
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
+	if (CurrentSnapshotData.valid)
+		dlist_delete(&CurrentSnapshotData.node);
 	GetSnapshotData(&CurrentSnapshotData);
+	valid_snapshots_push_tail(&CurrentSnapshotData);
 
 	return (Snapshot) &CurrentSnapshotData;
 }
@@ -372,7 +365,10 @@ GetLatestSnapshot(void)
 	if (!FirstSnapshotSet)
 		return GetTransactionSnapshot();
 
+	if (SecondarySnapshotData.valid)
+		dlist_delete(&SecondarySnapshotData.node);
 	GetSnapshotData(&SecondarySnapshotData);
+	valid_snapshots_push_tail(&SecondarySnapshotData);
 
 	return (Snapshot) &SecondarySnapshotData;
 }
@@ -436,7 +432,7 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		 * NB: it had better be impossible for this to throw error, since the
 		 * CatalogSnapshot pointer is already valid.
 		 */
-		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
+		valid_snapshots_push_tail(&CatalogSnapshotData);
 	}
 
 	return (Snapshot) &CatalogSnapshotData;
@@ -457,11 +453,22 @@ InvalidateCatalogSnapshot(void)
 {
 	if (CatalogSnapshotData.valid)
 	{
-		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
+		dlist_delete(&CatalogSnapshotData.node);
 		CatalogSnapshotData.valid = false;
-		SnapshotResetXmin();
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
 	}
+	if (!FirstXactSnapshotRegistered && CurrentSnapshotData.valid)
+	{
+		dlist_delete(&CurrentSnapshotData.node);
+		CurrentSnapshotData.valid = false;
+	}
+	if (SecondarySnapshotData.valid)
+	{
+		dlist_delete(&SecondarySnapshotData.node);
+		SecondarySnapshotData.valid = false;
+	}
+
+	SnapshotResetXmin();
 }
 
 /*
@@ -478,8 +485,7 @@ void
 InvalidateCatalogSnapshotConditionally(void)
 {
 	if (CatalogSnapshotData.valid &&
-		ActiveSnapshot == NULL &&
-		pairingheap_is_singular(&RegisteredSnapshots))
+		dlist_head_node(&ValidSnapshots) == &CatalogSnapshotData.node)
 		InvalidateCatalogSnapshot();
 }
 
@@ -589,9 +595,8 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 											   sourcepid);
 		/* Mark it as "registered" */
 		FirstXactSnapshotRegistered = true;
-		CurrentSnapshotData.regd_count++;
-		pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 	}
+	valid_snapshots_push_tail(&CurrentSnapshotData);
 
 	FirstSnapshotSet = true;
 }
@@ -707,8 +712,13 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 
 	newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
 
-	newactive->as_snap = origsnap->copied ?
-		origsnap : CopyMVCCSnapshot(origsnap);
+	if (!origsnap->copied)
+	{
+		newactive->as_snap = CopyMVCCSnapshot(origsnap);
+		dlist_insert_after(&origsnap->node, &newactive->as_snap->node);
+	}
+	else
+		newactive->as_snap = origsnap;
 
 	newactive->as_next = ActiveSnapshot;
 	newactive->as_level = snap_level;
@@ -729,8 +739,13 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 void
 PushCopiedSnapshot(Snapshot snapshot)
 {
+	MVCCSnapshot copy;
+
 	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
-	PushActiveSnapshot((Snapshot) CopyMVCCSnapshot(&snapshot->mvcc));
+
+	copy = CopyMVCCSnapshot(&snapshot->mvcc);
+	dlist_insert_after(&snapshot->mvcc.node, &copy->node);
+	PushActiveSnapshot((Snapshot) copy);
 }
 
 /*
@@ -783,7 +798,10 @@ PopActiveSnapshot(void)
 
 	if (ActiveSnapshot->as_snap->active_count == 0 &&
 		ActiveSnapshot->as_snap->regd_count == 0)
+	{
+		dlist_delete(&ActiveSnapshot->as_snap->node);
 		FreeMVCCSnapshot(ActiveSnapshot->as_snap);
+	}
 
 	pfree(ActiveSnapshot);
 	ActiveSnapshot = newstack;
@@ -857,16 +875,17 @@ RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 	Assert(snapshot->valid);
 
 	/* Static snapshot?  Create a persistent copy */
-	snapshot = snapshot->copied ? snapshot : CopyMVCCSnapshot(snapshot);
+	if (!snapshot->copied)
+	{
+		snapshot = CopyMVCCSnapshot(snapshot);
+		dlist_insert_after(&orig_snapshot->mvcc.node, &snapshot->node);
+	}
 
 	/* and tell resowner.c about it */
 	ResourceOwnerEnlarge(owner);
 	snapshot->regd_count++;
 	ResourceOwnerRememberSnapshot(owner, (Snapshot) snapshot);
 
-	if (snapshot->regd_count == 1)
-		pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
-
 	return (Snapshot) snapshot;
 }
 
@@ -908,14 +927,12 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 		MVCCSnapshot mvccsnap = &snapshot->mvcc;
 
 		Assert(mvccsnap->regd_count > 0);
-		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
+		Assert(!dlist_is_empty(&ValidSnapshots));
 
 		mvccsnap->regd_count--;
-		if (mvccsnap->regd_count == 0)
-			pairingheap_remove(&RegisteredSnapshots, &mvccsnap->ph_node);
-
 		if (mvccsnap->regd_count == 0 && mvccsnap->active_count == 0)
 		{
+			dlist_delete(&mvccsnap->node);
 			FreeMVCCSnapshot(mvccsnap);
 			SnapshotResetXmin();
 		}
@@ -940,24 +957,6 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 		elog(ERROR, "registered snapshot has unexpected type");
 }
 
-/*
- * Comparison function for RegisteredSnapshots heap.  Snapshots are ordered
- * by xmin, so that the snapshot with smallest xmin is at the top.
- */
-static int
-xmin_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg)
-{
-	const MVCCSnapshotData *asnap = pairingheap_const_container(MVCCSnapshotData, ph_node, a);
-	const MVCCSnapshotData *bsnap = pairingheap_const_container(MVCCSnapshotData, ph_node, b);
-
-	if (TransactionIdPrecedes(asnap->xmin, bsnap->xmin))
-		return 1;
-	else if (TransactionIdFollows(asnap->xmin, bsnap->xmin))
-		return -1;
-	else
-		return 0;
-}
-
 /*
  * SnapshotResetXmin
  *
@@ -979,21 +978,27 @@ SnapshotResetXmin(void)
 	/*
 	 * Invalidate these static snapshots so that we can advance xmin.
 	 */
-	if (!FirstXactSnapshotRegistered)
+	if (!FirstXactSnapshotRegistered && CurrentSnapshotData.valid)
+	{
+		dlist_delete(&CurrentSnapshotData.node);
 		CurrentSnapshotData.valid = false;
-	SecondarySnapshotData.valid = false;
+	}
+	if (SecondarySnapshotData.valid)
+	{
+		dlist_delete(&SecondarySnapshotData.node);
+		SecondarySnapshotData.valid = false;
+	}
 
 	if (ActiveSnapshot != NULL)
 		return;
 
-	if (pairingheap_is_empty(&RegisteredSnapshots))
+	if (dlist_is_empty(&ValidSnapshots))
 	{
 		MyProc->xmin = TransactionXmin = InvalidTransactionId;
 		return;
 	}
 
-	minSnapshot = pairingheap_container(MVCCSnapshotData, ph_node,
-										pairingheap_first(&RegisteredSnapshots));
+	minSnapshot = dlist_head_element(MVCCSnapshotData, node, &ValidSnapshots);
 
 	if (TransactionIdPrecedes(MyProc->xmin, minSnapshot->xmin))
 		MyProc->xmin = TransactionXmin = minSnapshot->xmin;
@@ -1042,7 +1047,10 @@ AtSubAbort_Snapshot(int level)
 
 		if (ActiveSnapshot->as_snap->active_count == 0 &&
 			ActiveSnapshot->as_snap->regd_count == 0)
+		{
+			dlist_delete(&ActiveSnapshot->as_snap->node);
 			FreeMVCCSnapshot(ActiveSnapshot->as_snap);
+		}
 
 		/* and free the stack element */
 		pfree(ActiveSnapshot);
@@ -1060,23 +1068,6 @@ AtSubAbort_Snapshot(int level)
 void
 AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 {
-	/*
-	 * In transaction-snapshot mode we must release our privately-managed
-	 * reference to the transaction snapshot.  We must remove it from
-	 * RegisteredSnapshots to keep the check below happy.  But we don't bother
-	 * to do FreeMVCCSnapshot, for two reasons: the memory will go away with
-	 * TopTransactionContext anyway, and if someone has left the snapshot
-	 * stacked as active, we don't want the code below to be chasing through a
-	 * dangling pointer.
-	 */
-	if (FirstXactSnapshotRegistered)
-	{
-		Assert(CurrentSnapshotData.regd_count > 0);
-		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
-		pairingheap_remove(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
-		FirstXactSnapshotRegistered = false;
-	}
-
 	/*
 	 * If we exported any snapshots, clean them up.
 	 */
@@ -1089,8 +1080,8 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 		 * it's too late to abort the transaction, and (2) leaving a leaked
 		 * file around has little real consequence anyway.
 		 *
-		 * We also need to remove the snapshots from RegisteredSnapshots to
-		 * prevent a warning below.
+		 * We also need to remove the snapshots from ValidSnapshots to prevent
+		 * a warning below.
 		 *
 		 * As with the FirstXactSnapshot, we don't need to free resources of
 		 * the snapshot itself as it will go away with the memory context.
@@ -1103,22 +1094,35 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 				elog(WARNING, "could not unlink file \"%s\": %m",
 					 esnap->snapfile);
 
-			pairingheap_remove(&RegisteredSnapshots,
-							   &esnap->snapshot->ph_node);
+			dlist_delete(&esnap->snapshot->node);
 		}
 
 		exportedSnapshots = NIL;
 	}
 
-	/* Drop catalog snapshot if any */
-	InvalidateCatalogSnapshot();
+	/* Drop all static snapshot */
+	if (CatalogSnapshotData.valid)
+	{
+		dlist_delete(&CatalogSnapshotData.node);
+		CatalogSnapshotData.valid = false;
+	}
+	if (CurrentSnapshotData.valid)
+	{
+		dlist_delete(&CurrentSnapshotData.node);
+		CurrentSnapshotData.valid = false;
+	}
+	if (SecondarySnapshotData.valid)
+	{
+		dlist_delete(&SecondarySnapshotData.node);
+		SecondarySnapshotData.valid = false;
+	}
 
 	/* On commit, complain about leftover snapshots */
 	if (isCommit)
 	{
 		ActiveSnapshotElt *active;
 
-		if (!pairingheap_is_empty(&RegisteredSnapshots))
+		if (!dlist_is_empty(&ValidSnapshots))
 			elog(WARNING, "registered snapshots seem to remain after cleanup");
 
 		/* complain about unpopped active snapshots */
@@ -1131,11 +1135,12 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * it'll go away with TopTransactionContext.
 	 */
 	ActiveSnapshot = NULL;
-	pairingheap_reset(&RegisteredSnapshots);
+	dlist_init(&ValidSnapshots);
 
 	CurrentSnapshotData.valid = false;
 	SecondarySnapshotData.valid = false;
 	FirstSnapshotSet = false;
+	FirstXactSnapshotRegistered = false;
 
 	/*
 	 * During normal commit processing, we call ProcArrayEndTransaction() to
@@ -1158,6 +1163,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 char *
 ExportSnapshot(MVCCSnapshot snapshot)
 {
+	MVCCSnapshot orig_snapshot;
 	TransactionId topXid;
 	TransactionId *children;
 	ExportedSnapshot *esnap;
@@ -1220,7 +1226,8 @@ ExportSnapshot(MVCCSnapshot snapshot)
 	 * ensure that the snapshot's xmin is honored for the rest of the
 	 * transaction.
 	 */
-	snapshot = CopyMVCCSnapshot(snapshot);
+	orig_snapshot = snapshot;
+	snapshot = CopyMVCCSnapshot(orig_snapshot);
 
 	oldcxt = MemoryContextSwitchTo(TopTransactionContext);
 	esnap = palloc_object(ExportedSnapshot);
@@ -1230,7 +1237,10 @@ ExportSnapshot(MVCCSnapshot snapshot)
 	MemoryContextSwitchTo(oldcxt);
 
 	snapshot->regd_count++;
-	pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
+	if (orig_snapshot->regd_count > 0)
+		dlist_insert_after(&orig_snapshot->node, &snapshot->node);
+	else
+		valid_snapshots_push_tail(snapshot);
 
 	/*
 	 * Fill buf with a text serialization of the snapshot, plus identification
@@ -1660,7 +1670,7 @@ DeleteAllExportedSnapshotFiles(void)
 
 /*
  * ThereAreNoPriorRegisteredSnapshots
- *		Is the registered snapshot count less than or equal to one?
+ *		Are there any snapshots other than the current active snapshot?
  *
  * Don't use this to settle important decisions.  While zero registrations and
  * no ActiveSnapshot would confirm a certain idleness, the system makes no
@@ -1669,11 +1679,25 @@ DeleteAllExportedSnapshotFiles(void)
 bool
 ThereAreNoPriorRegisteredSnapshots(void)
 {
-	if (pairingheap_is_empty(&RegisteredSnapshots) ||
-		pairingheap_is_singular(&RegisteredSnapshots))
-		return true;
+	dlist_iter	iter;
 
-	return false;
+	dlist_foreach(iter, &ValidSnapshots)
+	{
+		MVCCSnapshot cur = dlist_container(MVCCSnapshotData, node, iter.cur);
+
+		if (FirstXactSnapshotRegistered)
+		{
+			Assert(CurrentSnapshotData.valid);
+			if (cur != &CurrentSnapshotData)
+				continue;
+		}
+		if (ActiveSnapshot && cur == ActiveSnapshot->as_snap)
+			continue;
+
+		return false;
+	}
+
+	return true;
 }
 
 /*
@@ -1691,15 +1715,18 @@ HaveRegisteredOrActiveSnapshot(void)
 		return true;
 
 	/*
-	 * The catalog snapshot is in RegisteredSnapshots when valid, but can be
+	 * The catalog snapshot is in ValidSnapshots when valid, but can be
 	 * removed at any time due to invalidation processing. If explicitly
-	 * registered more than one snapshot has to be in RegisteredSnapshots.
+	 * registered more than one snapshot has to be in ValidSnapshots.
 	 */
 	if (CatalogSnapshotData.valid &&
-		pairingheap_is_singular(&RegisteredSnapshots))
+		dlist_head_node(&ValidSnapshots) == &CatalogSnapshotData.node &&
+		dlist_tail_node(&ValidSnapshots) == &CatalogSnapshotData.node)
+	{
 		return false;
+	}
 
-	return !pairingheap_is_empty(&RegisteredSnapshots);
+	return !dlist_is_empty(&ValidSnapshots);
 }
 
 
@@ -1891,7 +1918,7 @@ RestoreSnapshot(char *start_address)
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	snapshot->regd_count++;
 	ResourceOwnerRememberSnapshot(CurrentResourceOwner, (Snapshot) snapshot);
-	pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
+	valid_snapshots_push_out_of_order(snapshot);
 
 	return snapshot;
 }
@@ -2019,3 +2046,45 @@ ResOwnerReleaseSnapshot(Datum res)
 {
 	UnregisterSnapshotNoOwner((Snapshot) DatumGetPointer(res));
 }
+
+
+/* Helper functions to manipulate the ValidSnapshots list */
+
+/* dlist_push_tail, with assertion that the list stays ordered by xmin */
+static void
+valid_snapshots_push_tail(MVCCSnapshot snapshot)
+{
+#ifdef USE_ASSERT_CHECKING
+	if (!dlist_is_empty(&ValidSnapshots))
+	{
+		MVCCSnapshot tail = dlist_tail_element(MVCCSnapshotData, node, &ValidSnapshots);
+
+		Assert(TransactionIdFollowsOrEquals(snapshot->xmin, tail->xmin));
+	}
+#endif
+	dlist_push_tail(&ValidSnapshots, &snapshot->node);
+}
+
+/*
+ * Add an entry to the right position in the list, keeping it ordered by xmin.
+ *
+ * This is O(n), but that's OK because it's only used in rare occasions, when
+ * the list is small.
+ */
+static void
+valid_snapshots_push_out_of_order(MVCCSnapshot snapshot)
+{
+	dlist_iter	iter;
+
+	dlist_foreach(iter, &ValidSnapshots)
+	{
+		MVCCSnapshot cur = dlist_container(MVCCSnapshotData, node, iter.cur);
+
+		if (TransactionIdFollowsOrEquals(snapshot->xmin, cur->xmin))
+		{
+			dlist_insert_after(&cur->node, &snapshot->node);
+			return;
+		}
+	}
+	dlist_push_tail(&ValidSnapshots, &snapshot->node);
+}
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 083af5962a8..13ad9250fe0 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -17,6 +17,7 @@
 #include "access/itup.h"
 #include "access/spgist.h"
 #include "catalog/pg_am_d.h"
+#include "lib/pairingheap.h"
 #include "nodes/tidbitmap.h"
 #include "storage/buf.h"
 #include "utils/geo_decls.h"
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 1697c6df856..44b3b20f73c 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -13,7 +13,7 @@
 #ifndef SNAPSHOT_H
 #define SNAPSHOT_H
 
-#include "lib/pairingheap.h"
+#include "lib/ilist.h"
 
 
 /*
@@ -169,8 +169,8 @@ typedef struct MVCCSnapshotData
 	 * Book-keeping information, used by the snapshot manager
 	 */
 	uint32		active_count;	/* refcount on ActiveSnapshot stack */
-	uint32		regd_count;		/* refcount on RegisteredSnapshots */
-	pairingheap_node ph_node;	/* link in the RegisteredSnapshots heap */
+	uint32		regd_count;		/* refcount of registrations in resowners */
+	dlist_node	node;			/* link in ValidSnapshots */
 
 	/*
 	 * The transaction completion count at the time GetSnapshotData() built
-- 
2.47.3



  [text/x-patch] 0008-WIP-Don-t-serialize-the-snapshot-for-parallel-scans.patch (25.9K, ../../[email protected]/9-0008-WIP-Don-t-serialize-the-snapshot-for-parallel-scans.patch)
  download | inline diff:
From fa726baf070a5e5708dfbe36757e1c4f66ecee8a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 18 Dec 2025 23:54:17 +0200
Subject: [PATCH 8/9] WIP: Don't serialize the snapshot for parallel scans

When performing a parallel table scan or index scan, we serialized the
snapshot to use and passed it to the parallel worker. However in
practice, in the executor we always pass the executor's snapshot,
i.e. EState->es_snapshot, which is also always the active
snapshot. The only other place where we use parallel scans is parallel
index builds, and they always pass either SnapshotAny or the current
transaction snapshot. We're not really using on the capability to use
an arbitrary snapshot.

Stop serializing the snapshot as part of the parallel scan
desc. Instead, require the caller to pass it as an argument to
table_beginscan_parallel() in the worker processes. That moves the
responsibility for serializing or otherwise getting the snapshot in
each worker process to the calling code. That's not a problem for the
executor, because the active snapshot is already serialized elsewhere,
and for parallel index scans, we also already have access to the
transaction snapshot. This simplifies the code a little, and
eliminates the overhead of serializing the snapshot.

The reason that this is part of this patch set is that the previous
commit made RestoreSnapshot() slower when there are already snapshots
registered (O(n)). This removes the RestoreSnapshot() calls that
could've become slow, although it'd probably be fine in practice.
---
 src/backend/access/brin/brin.c           | 23 +++++----
 src/backend/access/gin/gininsert.c       | 23 +++++----
 src/backend/access/index/indexam.c       | 16 ++-----
 src/backend/access/nbtree/nbtsort.c      | 23 +++++----
 src/backend/access/table/tableam.c       | 61 ++++--------------------
 src/backend/executor/nodeIndexonlyscan.c |  3 ++
 src/backend/executor/nodeIndexscan.c     |  3 ++
 src/backend/executor/nodeSeqscan.c       | 15 +++---
 src/backend/executor/nodeTidrangescan.c  | 13 ++---
 src/include/access/genam.h               |  1 +
 src/include/access/relscan.h             |  3 --
 src/include/access/tableam.h             | 11 +++--
 12 files changed, 84 insertions(+), 111 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 45d306037a4..d3b5b7e47a8 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -231,7 +231,7 @@ static void brin_fill_empty_ranges(BrinBuildState *state,
 static void _brin_begin_parallel(BrinBuildState *buildstate, Relation heap, Relation index,
 								 bool isconcurrent, int request);
 static void _brin_end_parallel(BrinLeader *brinleader, BrinBuildState *state);
-static Size _brin_parallel_estimate_shared(Relation heap, Snapshot snapshot);
+static Size _brin_parallel_estimate_shared(Relation heap);
 static double _brin_parallel_heapscan(BrinBuildState *state);
 static double _brin_parallel_merge(BrinBuildState *state);
 static void _brin_leader_participate_as_worker(BrinBuildState *buildstate,
@@ -2420,7 +2420,7 @@ _brin_begin_parallel(BrinBuildState *buildstate, Relation heap, Relation index,
 	/*
 	 * Estimate size for our own PARALLEL_KEY_BRIN_SHARED workspace.
 	 */
-	estbrinshared = _brin_parallel_estimate_shared(heap, snapshot);
+	estbrinshared = _brin_parallel_estimate_shared(heap);
 	shm_toc_estimate_chunk(&pcxt->estimator, estbrinshared);
 	estsort = tuplesort_estimate_shared(scantuplesortstates);
 	shm_toc_estimate_chunk(&pcxt->estimator, estsort);
@@ -2483,8 +2483,7 @@ _brin_begin_parallel(BrinBuildState *buildstate, Relation heap, Relation index,
 	brinshared->indtuples = 0.0;
 
 	table_parallelscan_initialize(heap,
-								  ParallelTableScanFromBrinShared(brinshared),
-								  snapshot);
+								  ParallelTableScanFromBrinShared(brinshared));
 
 	/*
 	 * Store shared tuplesort-private state, for which we reserved space.
@@ -2775,14 +2774,14 @@ _brin_parallel_merge(BrinBuildState *state)
 
 /*
  * Returns size of shared memory required to store state for a parallel
- * brin index build based on the snapshot its parallel scan will use.
+ * brin index build.
  */
 static Size
-_brin_parallel_estimate_shared(Relation heap, Snapshot snapshot)
+_brin_parallel_estimate_shared(Relation heap)
 {
 	/* c.f. shm_toc_allocate as to why BUFFERALIGN is used */
 	return add_size(BUFFERALIGN(sizeof(BrinShared)),
-					table_parallelscan_estimate(heap, snapshot));
+					table_parallelscan_estimate(heap));
 }
 
 /*
@@ -2823,6 +2822,7 @@ _brin_parallel_scan_and_build(BrinBuildState *state,
 							  int sortmem, bool progress)
 {
 	SortCoordinate coordinate;
+	Snapshot	snapshot;
 	TableScanDesc scan;
 	double		reltuples;
 	IndexInfo  *indexInfo;
@@ -2837,12 +2837,19 @@ _brin_parallel_scan_and_build(BrinBuildState *state,
 	state->bs_sortstate = tuplesort_begin_index_brin(sortmem, coordinate,
 													 TUPLESORT_NONE);
 
+	/* Use the right snapshot, per the same logic as in _brin_begin_parallel() */
+	if (!brinshared->isconcurrent)
+		snapshot = SnapshotAny;
+	else
+		snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
 	/* Join parallel scan */
 	indexInfo = BuildIndexInfo(index);
 	indexInfo->ii_Concurrent = brinshared->isconcurrent;
 
 	scan = table_beginscan_parallel(heap,
-									ParallelTableScanFromBrinShared(brinshared));
+									ParallelTableScanFromBrinShared(brinshared),
+									snapshot);
 
 	reltuples = table_index_build_scan(heap, index, indexInfo, true, true,
 									   brinbuildCallbackParallel, state, scan);
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index df30dcc0228..ae71bcb716c 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -181,7 +181,7 @@ typedef struct
 static void _gin_begin_parallel(GinBuildState *buildstate, Relation heap, Relation index,
 								bool isconcurrent, int request);
 static void _gin_end_parallel(GinLeader *ginleader, GinBuildState *state);
-static Size _gin_parallel_estimate_shared(Relation heap, Snapshot snapshot);
+static Size _gin_parallel_estimate_shared(Relation heap);
 static double _gin_parallel_heapscan(GinBuildState *state);
 static double _gin_parallel_merge(GinBuildState *state);
 static void _gin_leader_participate_as_worker(GinBuildState *buildstate,
@@ -969,7 +969,7 @@ _gin_begin_parallel(GinBuildState *buildstate, Relation heap, Relation index,
 	/*
 	 * Estimate size for our own PARALLEL_KEY_GIN_SHARED workspace.
 	 */
-	estginshared = _gin_parallel_estimate_shared(heap, snapshot);
+	estginshared = _gin_parallel_estimate_shared(heap);
 	shm_toc_estimate_chunk(&pcxt->estimator, estginshared);
 	estsort = tuplesort_estimate_shared(scantuplesortstates);
 	shm_toc_estimate_chunk(&pcxt->estimator, estsort);
@@ -1031,8 +1031,7 @@ _gin_begin_parallel(GinBuildState *buildstate, Relation heap, Relation index,
 	ginshared->indtuples = 0.0;
 
 	table_parallelscan_initialize(heap,
-								  ParallelTableScanFromGinBuildShared(ginshared),
-								  snapshot);
+								  ParallelTableScanFromGinBuildShared(ginshared));
 
 	/*
 	 * Store shared tuplesort-private state, for which we reserved space.
@@ -1794,14 +1793,14 @@ _gin_parallel_merge(GinBuildState *state)
 
 /*
  * Returns size of shared memory required to store state for a parallel
- * gin index build based on the snapshot its parallel scan will use.
+ * gin index build.
  */
 static Size
-_gin_parallel_estimate_shared(Relation heap, Snapshot snapshot)
+_gin_parallel_estimate_shared(Relation heap)
 {
 	/* c.f. shm_toc_allocate as to why BUFFERALIGN is used */
 	return add_size(BUFFERALIGN(sizeof(GinBuildShared)),
-					table_parallelscan_estimate(heap, snapshot));
+					table_parallelscan_estimate(heap));
 }
 
 /*
@@ -2025,6 +2024,7 @@ _gin_parallel_scan_and_build(GinBuildState *state,
 							 int sortmem, bool progress)
 {
 	SortCoordinate coordinate;
+	Snapshot	snapshot;
 	TableScanDesc scan;
 	double		reltuples;
 	IndexInfo  *indexInfo;
@@ -2057,8 +2057,15 @@ _gin_parallel_scan_and_build(GinBuildState *state,
 	indexInfo = BuildIndexInfo(index);
 	indexInfo->ii_Concurrent = ginshared->isconcurrent;
 
+	/* Use the right snapshot, per the same logic as in _gin_begin_parallel() */
+	if (!ginshared->isconcurrent)
+		snapshot = SnapshotAny;
+	else
+		snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
 	scan = table_beginscan_parallel(heap,
-									ParallelTableScanFromGinBuildShared(ginshared));
+									ParallelTableScanFromGinBuildShared(ginshared),
+									snapshot);
 
 	reltuples = table_index_build_scan(heap, index, indexInfo, true, progress,
 									   ginBuildCallbackParallel, state, scan);
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 3828622665c..9d032814e45 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -303,8 +303,6 @@ index_beginscan_bitmap(Relation indexRelation,
 {
 	IndexScanDesc scan;
 
-	Assert(snapshot != InvalidSnapshot);
-
 	scan = index_beginscan_internal(indexRelation, nkeys, 0, snapshot, NULL, false);
 
 	/*
@@ -478,8 +476,7 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 
 	RELATION_CHECKS;
 
-	nbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data);
-	nbytes = add_size(nbytes, EstimateSnapshotSpace(&snapshot->mvcc));
+	nbytes = sizeof(ParallelIndexScanDescData);
 	nbytes = MAXALIGN(nbytes);
 
 	if (instrument)
@@ -528,17 +525,14 @@ index_parallelscan_initialize(Relation heapRelation, Relation indexRelation,
 	Assert(instrument || parallel_aware);
 
 	RELATION_CHECKS;
-	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
 
-	offset = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data),
-					  EstimateSnapshotSpace((MVCCSnapshot) snapshot));
+	offset = sizeof(ParallelIndexScanDescData);
 	offset = MAXALIGN(offset);
 
 	target->ps_locator = heapRelation->rd_locator;
 	target->ps_indexlocator = indexRelation->rd_locator;
 	target->ps_offset_ins = 0;
 	target->ps_offset_am = 0;
-	SerializeSnapshot((MVCCSnapshot) snapshot, target->ps_snapshot_data);
 
 	if (instrument)
 	{
@@ -592,19 +586,19 @@ index_parallelrescan(IndexScanDesc scan)
  */
 IndexScanDesc
 index_beginscan_parallel(Relation heaprel, Relation indexrel,
+						 Snapshot snapshot,
 						 IndexScanInstrumentation *instrument,
 						 int nkeys, int norderbys,
 						 ParallelIndexScanDesc pscan)
 {
-	Snapshot	snapshot;
 	IndexScanDesc scan;
 
+	Assert(snapshot != InvalidSnapshot);
 	Assert(RelFileLocatorEquals(heaprel->rd_locator, pscan->ps_locator));
 	Assert(RelFileLocatorEquals(indexrel->rd_locator, pscan->ps_indexlocator));
 
-	snapshot = (Snapshot) RestoreSnapshot(pscan->ps_snapshot_data);
 	scan = index_beginscan_internal(indexrel, nkeys, norderbys, snapshot,
-									pscan, true);
+									pscan, false);
 
 	/*
 	 * Save additional parameters into the scandesc.  Everything else was set
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index d7695dc1108..b60f4807bde 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -279,7 +279,7 @@ static void _bt_load(BTWriteState *wstate,
 static void _bt_begin_parallel(BTBuildState *buildstate, bool isconcurrent,
 							   int request);
 static void _bt_end_parallel(BTLeader *btleader);
-static Size _bt_parallel_estimate_shared(Relation heap, Snapshot snapshot);
+static Size _bt_parallel_estimate_shared(Relation heap);
 static double _bt_parallel_heapscan(BTBuildState *buildstate,
 									bool *brokenhotchain);
 static void _bt_leader_participate_as_worker(BTBuildState *buildstate);
@@ -1441,7 +1441,7 @@ _bt_begin_parallel(BTBuildState *buildstate, bool isconcurrent, int request)
 	 * Estimate size for our own PARALLEL_KEY_BTREE_SHARED workspace, and
 	 * PARALLEL_KEY_TUPLESORT tuplesort workspace
 	 */
-	estbtshared = _bt_parallel_estimate_shared(btspool->heap, snapshot);
+	estbtshared = _bt_parallel_estimate_shared(btspool->heap);
 	shm_toc_estimate_chunk(&pcxt->estimator, estbtshared);
 	estsort = tuplesort_estimate_shared(scantuplesortstates);
 	shm_toc_estimate_chunk(&pcxt->estimator, estsort);
@@ -1515,8 +1515,7 @@ _bt_begin_parallel(BTBuildState *buildstate, bool isconcurrent, int request)
 	btshared->indtuples = 0.0;
 	btshared->brokenhotchain = false;
 	table_parallelscan_initialize(btspool->heap,
-								  ParallelTableScanFromBTShared(btshared),
-								  snapshot);
+								  ParallelTableScanFromBTShared(btshared));
 
 	/*
 	 * Store shared tuplesort-private state, for which we reserved space.
@@ -1628,14 +1627,14 @@ _bt_end_parallel(BTLeader *btleader)
 
 /*
  * Returns size of shared memory required to store state for a parallel
- * btree index build based on the snapshot its parallel scan will use.
+ * btree index build.
  */
 static Size
-_bt_parallel_estimate_shared(Relation heap, Snapshot snapshot)
+_bt_parallel_estimate_shared(Relation heap)
 {
 	/* c.f. shm_toc_allocate as to why BUFFERALIGN is used */
 	return add_size(BUFFERALIGN(sizeof(BTShared)),
-					table_parallelscan_estimate(heap, snapshot));
+					table_parallelscan_estimate(heap));
 }
 
 /*
@@ -1869,6 +1868,7 @@ _bt_parallel_scan_and_sort(BTSpool *btspool, BTSpool *btspool2,
 {
 	SortCoordinate coordinate;
 	BTBuildState buildstate;
+	Snapshot	snapshot;
 	TableScanDesc scan;
 	double		reltuples;
 	IndexInfo  *indexInfo;
@@ -1921,11 +1921,18 @@ _bt_parallel_scan_and_sort(BTSpool *btspool, BTSpool *btspool2,
 	buildstate.indtuples = 0;
 	buildstate.btleader = NULL;
 
+	/* Use the right snapshot, per the same logic as in _bt_begin_parallel() */
+	if (!btshared->isconcurrent)
+		snapshot = SnapshotAny;
+	else
+		snapshot = RegisterSnapshot(GetTransactionSnapshot());
+
 	/* Join parallel scan */
 	indexInfo = BuildIndexInfo(btspool->index);
 	indexInfo->ii_Concurrent = btshared->isconcurrent;
 	scan = table_beginscan_parallel(btspool->heap,
-									ParallelTableScanFromBTShared(btshared));
+									ParallelTableScanFromBTShared(btshared),
+									snapshot);
 	reltuples = table_index_build_scan(btspool->heap, btspool->index, indexInfo,
 									   true, progress, _bt_build_callback,
 									   &buildstate, scan);
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index c8db2918f40..61f5ae13474 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -128,70 +128,39 @@ table_beginscan_catalog(Relation relation, int nkeys, ScanKeyData *key)
  */
 
 Size
-table_parallelscan_estimate(Relation rel, Snapshot snapshot)
+table_parallelscan_estimate(Relation rel)
 {
-	Size		sz = 0;
+	Size		sz;
 
-	if (IsMVCCSnapshot(snapshot))
-		sz = add_size(sz, EstimateSnapshotSpace((MVCCSnapshot) snapshot));
-	else
-		Assert(snapshot == SnapshotAny);
-
-	sz = add_size(sz, rel->rd_tableam->parallelscan_estimate(rel));
+	sz = rel->rd_tableam->parallelscan_estimate(rel);
 
 	return sz;
 }
 
 void
-table_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan,
-							  Snapshot snapshot)
+table_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan)
 {
-	Size		snapshot_off = rel->rd_tableam->parallelscan_initialize(rel, pscan);
-
-	pscan->phs_snapshot_off = snapshot_off;
-
-	if (IsMVCCSnapshot(snapshot))
-	{
-		SerializeSnapshot((MVCCSnapshot) snapshot, (char *) pscan + pscan->phs_snapshot_off);
-		pscan->phs_snapshot_any = false;
-	}
-	else
-	{
-		Assert(snapshot == SnapshotAny);
-		pscan->phs_snapshot_any = true;
-	}
+	(void) rel->rd_tableam->parallelscan_initialize(rel, pscan);
 }
 
 TableScanDesc
-table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan)
+table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan,
+						 Snapshot snapshot)
 {
-	Snapshot	snapshot;
 	uint32		flags = SO_TYPE_SEQSCAN |
 		SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE;
 
 	Assert(RelFileLocatorEquals(relation->rd_locator, pscan->phs_locator));
 
-	if (!pscan->phs_snapshot_any)
-	{
-		/* Snapshot was serialized -- restore it */
-		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		flags |= SO_TEMP_SNAPSHOT;
-	}
-	else
-	{
-		/* SnapshotAny passed by caller (not serialized) */
-		snapshot = SnapshotAny;
-	}
-
 	return relation->rd_tableam->scan_begin(relation, snapshot, 0, NULL,
 											pscan, flags);
 }
 
 TableScanDesc
 table_beginscan_parallel_tidrange(Relation relation,
-								  ParallelTableScanDesc pscan)
+								  ParallelTableScanDesc pscan,
+								  Snapshot snapshot)
 {
-	Snapshot	snapshot;
 	uint32		flags = SO_TYPE_TIDRANGESCAN | SO_ALLOW_PAGEMODE;
 	TableScanDesc sscan;
 
@@ -200,18 +169,6 @@ table_beginscan_parallel_tidrange(Relation relation,
 	/* disable syncscan in parallel tid range scan. */
 	pscan->phs_syncscan = false;
 
-	if (!pscan->phs_snapshot_any)
-	{
-		/* Snapshot was serialized -- restore it */
-		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		flags |= SO_TEMP_SNAPSHOT;
-	}
-	else
-	{
-		/* SnapshotAny passed by caller (not serialized) */
-		snapshot = SnapshotAny;
-	}
-
 	sscan = relation->rd_tableam->scan_begin(relation, snapshot, 0, NULL,
 											 pscan, flags);
 	return sscan;
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index 6bea42f128f..de4c1dd692c 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -785,6 +785,7 @@ ExecIndexOnlyScanInitializeDSM(IndexOnlyScanState *node,
 	node->ioss_ScanDesc =
 		index_beginscan_parallel(node->ss.ss_currentRelation,
 								 node->ioss_RelationDesc,
+								 estate->es_snapshot,
 								 &node->ioss_Instrument,
 								 node->ioss_NumScanKeys,
 								 node->ioss_NumOrderByKeys,
@@ -826,6 +827,7 @@ void
 ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node,
 								  ParallelWorkerContext *pwcxt)
 {
+	EState	   *estate = node->ss.ps.state;
 	ParallelIndexScanDesc piscan;
 	bool		instrument = node->ss.ps.instrument != NULL;
 	bool		parallel_aware = node->ss.ps.plan->parallel_aware;
@@ -851,6 +853,7 @@ ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node,
 	node->ioss_ScanDesc =
 		index_beginscan_parallel(node->ss.ss_currentRelation,
 								 node->ioss_RelationDesc,
+								 estate->es_snapshot,
 								 &node->ioss_Instrument,
 								 node->ioss_NumScanKeys,
 								 node->ioss_NumOrderByKeys,
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 72b135e5dcf..dff1aac452b 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -1720,6 +1720,7 @@ ExecIndexScanInitializeDSM(IndexScanState *node,
 	node->iss_ScanDesc =
 		index_beginscan_parallel(node->ss.ss_currentRelation,
 								 node->iss_RelationDesc,
+								 estate->es_snapshot,
 								 &node->iss_Instrument,
 								 node->iss_NumScanKeys,
 								 node->iss_NumOrderByKeys,
@@ -1759,6 +1760,7 @@ void
 ExecIndexScanInitializeWorker(IndexScanState *node,
 							  ParallelWorkerContext *pwcxt)
 {
+	EState	   *estate = node->ss.ps.state;
 	ParallelIndexScanDesc piscan;
 	bool		instrument = node->ss.ps.instrument != NULL;
 	bool		parallel_aware = node->ss.ps.plan->parallel_aware;
@@ -1784,6 +1786,7 @@ ExecIndexScanInitializeWorker(IndexScanState *node,
 	node->iss_ScanDesc =
 		index_beginscan_parallel(node->ss.ss_currentRelation,
 								 node->iss_RelationDesc,
+								 estate->es_snapshot,
 								 &node->iss_Instrument,
 								 node->iss_NumScanKeys,
 								 node->iss_NumOrderByKeys,
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 94047d29430..ebc3d35e11b 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -347,10 +347,7 @@ void
 ExecSeqScanEstimate(SeqScanState *node,
 					ParallelContext *pcxt)
 {
-	EState	   *estate = node->ss.ps.state;
-
-	node->pscan_len = table_parallelscan_estimate(node->ss.ss_currentRelation,
-												  estate->es_snapshot);
+	node->pscan_len = table_parallelscan_estimate(node->ss.ss_currentRelation);
 	shm_toc_estimate_chunk(&pcxt->estimator, node->pscan_len);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 }
@@ -370,11 +367,11 @@ ExecSeqScanInitializeDSM(SeqScanState *node,
 
 	pscan = shm_toc_allocate(pcxt->toc, node->pscan_len);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
-								  pscan,
-								  estate->es_snapshot);
+								  pscan);
 	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pscan);
 	node->ss.ss_currentScanDesc =
-		table_beginscan_parallel(node->ss.ss_currentRelation, pscan);
+		table_beginscan_parallel(node->ss.ss_currentRelation, pscan,
+								 estate->es_snapshot);
 }
 
 /* ----------------------------------------------------------------
@@ -403,9 +400,11 @@ void
 ExecSeqScanInitializeWorker(SeqScanState *node,
 							ParallelWorkerContext *pwcxt)
 {
+	EState	   *estate = node->ss.ps.state;
 	ParallelTableScanDesc pscan;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
-		table_beginscan_parallel(node->ss.ss_currentRelation, pscan);
+		table_beginscan_parallel(node->ss.ss_currentRelation, pscan,
+								 estate->es_snapshot);
 }
diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c
index 4ceb181d622..f80c3de630f 100644
--- a/src/backend/executor/nodeTidrangescan.c
+++ b/src/backend/executor/nodeTidrangescan.c
@@ -431,11 +431,8 @@ ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags)
 void
 ExecTidRangeScanEstimate(TidRangeScanState *node, ParallelContext *pcxt)
 {
-	EState	   *estate = node->ss.ps.state;
-
 	node->trss_pscanlen =
-		table_parallelscan_estimate(node->ss.ss_currentRelation,
-									estate->es_snapshot);
+		table_parallelscan_estimate(node->ss.ss_currentRelation);
 	shm_toc_estimate_chunk(&pcxt->estimator, node->trss_pscanlen);
 	shm_toc_estimate_keys(&pcxt->estimator, 1);
 }
@@ -454,12 +451,11 @@ ExecTidRangeScanInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt)
 
 	pscan = shm_toc_allocate(pcxt->toc, node->trss_pscanlen);
 	table_parallelscan_initialize(node->ss.ss_currentRelation,
-								  pscan,
-								  estate->es_snapshot);
+								  pscan);
 	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pscan);
 	node->ss.ss_currentScanDesc =
 		table_beginscan_parallel_tidrange(node->ss.ss_currentRelation,
-										  pscan);
+										  pscan, estate->es_snapshot);
 }
 
 /* ----------------------------------------------------------------
@@ -488,10 +484,11 @@ void
 ExecTidRangeScanInitializeWorker(TidRangeScanState *node,
 								 ParallelWorkerContext *pwcxt)
 {
+	EState	   *estate = node->ss.ps.state;
 	ParallelTableScanDesc pscan;
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
 		table_beginscan_parallel_tidrange(node->ss.ss_currentRelation,
-										  pscan);
+										  pscan, estate->es_snapshot);
 }
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 9200a22bd9f..411137997e1 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -201,6 +201,7 @@ extern void index_parallelscan_initialize(Relation heapRelation,
 extern void index_parallelrescan(IndexScanDesc scan);
 extern IndexScanDesc index_beginscan_parallel(Relation heaprel,
 											  Relation indexrel,
+											  Snapshot snapshot,
 											  IndexScanInstrumentation *instrument,
 											  int nkeys, int norderbys,
 											  ParallelIndexScanDesc pscan);
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 0f8fdcff782..1ba1a432618 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -81,8 +81,6 @@ typedef struct ParallelTableScanDescData
 {
 	RelFileLocator phs_locator; /* physical relation to scan */
 	bool		phs_syncscan;	/* report location to syncscan logic? */
-	bool		phs_snapshot_any;	/* SnapshotAny, not phs_snapshot_data? */
-	Size		phs_snapshot_off;	/* data for snapshot */
 } ParallelTableScanDescData;
 typedef struct ParallelTableScanDescData *ParallelTableScanDesc;
 
@@ -200,7 +198,6 @@ typedef struct ParallelIndexScanDescData
 	RelFileLocator ps_indexlocator; /* physical index relation to scan */
 	Size		ps_offset_ins;	/* Offset to SharedIndexScanInstrumentation */
 	Size		ps_offset_am;	/* Offset to am-specific structure */
-	char		ps_snapshot_data[FLEXIBLE_ARRAY_MEMBER];
 }			ParallelIndexScanDescData;
 
 struct TupleTableSlot;
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 2fa790b6bf5..00c2e2c6840 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -1108,7 +1108,7 @@ table_scan_getnextslot_tidrange(TableScanDesc sscan, ScanDirection direction,
  * Estimate the size of shared memory needed for a parallel scan of this
  * relation.
  */
-extern Size table_parallelscan_estimate(Relation rel, Snapshot snapshot);
+extern Size table_parallelscan_estimate(Relation rel);
 
 /*
  * Initialize ParallelTableScanDesc for a parallel scan of this
@@ -1117,8 +1117,7 @@ extern Size table_parallelscan_estimate(Relation rel, Snapshot snapshot);
  * individual workers attach via table_beginscan_parallel.
  */
 extern void table_parallelscan_initialize(Relation rel,
-										  ParallelTableScanDesc pscan,
-										  Snapshot snapshot);
+										  ParallelTableScanDesc pscan);
 
 /*
  * Begin a parallel scan. `pscan` needs to have been initialized with
@@ -1128,7 +1127,8 @@ extern void table_parallelscan_initialize(Relation rel,
  * Caller must hold a suitable lock on the relation.
  */
 extern TableScanDesc table_beginscan_parallel(Relation relation,
-											  ParallelTableScanDesc pscan);
+											  ParallelTableScanDesc pscan,
+											  Snapshot snapshot);
 
 /*
  * Begin a parallel tid range scan. `pscan` needs to have been initialized
@@ -1138,7 +1138,8 @@ extern TableScanDesc table_beginscan_parallel(Relation relation,
  * Caller must hold a suitable lock on the relation.
  */
 extern TableScanDesc table_beginscan_parallel_tidrange(Relation relation,
-													   ParallelTableScanDesc pscan);
+													   ParallelTableScanDesc pscan,
+													   Snapshot snapshot);
 
 /*
  * Restart a parallel scan.  Call this in the leader process.  Caller is
-- 
2.47.3



  [text/x-patch] 0009-WIP-Split-MVCCSnapshot-into-inner-and-outer-parts.patch (94.5K, ../../[email protected]/10-0009-WIP-Split-MVCCSnapshot-into-inner-and-outer-parts.patch)
  download | inline diff:
From b390dcb7af8cf6e52db76d64f2afe7ad07bf42cc Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 21:46:54 +0300
Subject: [PATCH 9/9] WIP: Split MVCCSnapshot into inner and outer parts

Split MVCCSnapshot into two parts: inner struct to hold the xmin, xmax
and XID arrays that determine which transactions are visible, and an
outer shell that includes the command ID and a pointer to the inner
struct. That way, the inner struct can be shared by snapshots derived
from the same original snapshot, just with different command IDs.

The inner struct, MVCCSnapshotShared, is reference counted separately
so that we can avoid copying it when pushing or registering a snapshot
for the first time. Also, GetMVCCSnapshotData() can reuse it more
aggressively: we always keep a pointer to the latest shared struct
(latestSnapshotShared), and GetMVCCSnapshotData() always tries to
reuse the same latest snapshot, regardless of whether it was called
from GetTransactionSnapshot(), GetLatestSnapshot(), or
GetCatalogSnapshot(). That avoids unnecessary copying. Snapshots are
usually small so that it doesn't matter, but it can help in extreme
cases where you have thousands of (sub-)XIDs in progress.

Now that the shared inner structs are reference counted, it seems
unnecessary to reference count the outer MVCCSnapshots
separately. That means that RegisterSnapshot() always makes a new
palloc'd copy of the outer struct, but that's pretty small. The
ActiveSnapshot stack entries now embed the outer struct directly, so
the 'active_count' is gone too.

The ValidSnapshots list now tracks the shared structs rather than the
outer snapshots. That's sufficient for finding the oldest xmin, but if
we ever wanted to also know the oldest command ID in use, we'd need to
track the outer structs instead.
---
 contrib/amcheck/verify_heapam.c             |   2 +-
 contrib/amcheck/verify_nbtree.c             |   2 +-
 src/backend/access/heap/heapam.c            |   2 +-
 src/backend/access/heap/heapam_handler.c    |   2 +-
 src/backend/access/heap/heapam_visibility.c |  18 +-
 src/backend/access/spgist/spgvacuum.c       |   2 +-
 src/backend/access/transam/README           |  26 +-
 src/backend/catalog/pg_inherits.c           |   6 +-
 src/backend/commands/async.c                |   2 +-
 src/backend/commands/indexcmds.c            |   4 +-
 src/backend/commands/tablecmds.c            |   2 +-
 src/backend/executor/execMain.c             |  12 +-
 src/backend/executor/execParallel.c         |   3 +-
 src/backend/partitioning/partdesc.c         |   2 +-
 src/backend/replication/logical/snapbuild.c |  40 +-
 src/backend/replication/walsender.c         |   2 +-
 src/backend/storage/ipc/procarray.c         | 136 ++---
 src/backend/storage/lmgr/predicate.c        | 109 ++--
 src/backend/utils/adt/xid8funcs.c           |   8 +-
 src/backend/utils/time/snapmgr.c            | 600 ++++++++++----------
 src/include/access/transam.h                |   4 +-
 src/include/storage/predicate.h             |   8 +-
 src/include/storage/proc.h                  |   2 +-
 src/include/storage/procarray.h             |   2 +-
 src/include/utils/snapmgr.h                 |  11 +-
 src/include/utils/snapshot.h                |  51 +-
 src/tools/pgindent/typedefs.list            |   2 +
 27 files changed, 537 insertions(+), 523 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 75907349f42..417322b3e5f 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -310,7 +310,7 @@ verify_heapam(PG_FUNCTION_ARGS)
 	 * Any xmin newer than the xmin of our snapshot can't become all-visible
 	 * while we're running.
 	 */
-	ctx.safe_xmin = GetTransactionSnapshot()->mvcc.xmin;
+	ctx.safe_xmin = GetTransactionSnapshot()->mvcc.shared->xmin;
 
 	/*
 	 * If we report corruption when not examining some individual attribute,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index b91ac1f3fdf..49e70980520 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -454,7 +454,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin &&
 			!TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data),
-								   state->snapshot->mvcc.xmin))
+								   state->snapshot->mvcc.shared->xmin))
 			ereport(ERROR,
 					errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 					errmsg("index \"%s\" cannot be verified using transaction snapshot",
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 1fa424fa131..d273b964fa4 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -617,7 +617,7 @@ heap_prepare_pagescan(TableScanDesc sscan)
 	 * tuple for visibility the hard way.
 	 */
 	all_visible = PageIsAllVisible(page) &&
-		(snapshot->snapshot_type != SNAPSHOT_MVCC || !snapshot->mvcc.takenDuringRecovery);
+		(snapshot->snapshot_type != SNAPSHOT_MVCC || !snapshot->mvcc.shared->takenDuringRecovery);
 	check_serializable =
 		CheckForSerializableConflictOutNeeded(scan->rs_base.rs_rd, snapshot);
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 26b72a88f65..bd6083fa72e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2282,7 +2282,7 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
 
 	page = BufferGetPage(hscan->rs_cbuf);
 	all_visible = PageIsAllVisible(page) &&
-		(scan->rs_snapshot->snapshot_type != SNAPSHOT_MVCC || !scan->rs_snapshot->mvcc.takenDuringRecovery);
+		(scan->rs_snapshot->snapshot_type != SNAPSHOT_MVCC || !scan->rs_snapshot->mvcc.shared->takenDuringRecovery);
 	maxoffset = PageGetMaxOffsetNumber(page);
 
 	for (;;)
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index f5d69b558f1..07f155498d4 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -19,7 +19,7 @@
  * That fixes that problem, but it also means there is a window where
  * TransactionIdIsInProgress and TransactionIdDidCommit will both return true.
  * If we check only TransactionIdDidCommit, we could consider a tuple
- * committed when a later GetSnapshotData call will still think the
+ * committed when a later GetMVCCSnapshotData call will still think the
  * originating transaction is in progress, which leads to application-level
  * inconsistency.  The upshot is that we gotta check TransactionIdIsInProgress
  * first in all code paths, except for a few cases where we are looking at
@@ -969,7 +969,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 	 * get invalidated while it's still in use, and this is a convenient place
 	 * to check for that.
 	 */
-	Assert(snapshot->regd_count > 0 || snapshot->active_count > 0);
+	Assert(snapshot->kind == SNAPSHOT_ACTIVE || snapshot->kind == SNAPSHOT_REGISTERED);
 
 	Assert(ItemPointerIsValid(&htup->t_self));
 	Assert(htup->t_tableOid != InvalidOid);
@@ -986,7 +986,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 
 			if (TransactionIdIsCurrentTransactionId(xvac))
 				return false;
-			if (!XidInMVCCSnapshot(xvac, snapshot))
+			if (!XidInMVCCSnapshot(xvac, snapshot->shared))
 			{
 				if (TransactionIdDidCommit(xvac))
 				{
@@ -1005,7 +1005,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 
 			if (!TransactionIdIsCurrentTransactionId(xvac))
 			{
-				if (XidInMVCCSnapshot(xvac, snapshot))
+				if (XidInMVCCSnapshot(xvac, snapshot->shared))
 					return false;
 				if (TransactionIdDidCommit(xvac))
 					SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,
@@ -1060,7 +1060,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 			else
 				return false;	/* deleted before scan started */
 		}
-		else if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
+		else if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot->shared))
 			return false;
 		else if (TransactionIdDidCommit(HeapTupleHeaderGetRawXmin(tuple)))
 			SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED,
@@ -1077,7 +1077,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 	{
 		/* xmin is committed, but maybe not according to our snapshot */
 		if (!HeapTupleHeaderXminFrozen(tuple) &&
-			XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
+			XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot->shared))
 			return false;		/* treat as still in progress */
 	}
 
@@ -1108,7 +1108,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 			else
 				return false;	/* deleted before scan started */
 		}
-		if (XidInMVCCSnapshot(xmax, snapshot))
+		if (XidInMVCCSnapshot(xmax, snapshot->shared))
 			return true;
 		if (TransactionIdDidCommit(xmax))
 			return false;		/* updating transaction committed */
@@ -1126,7 +1126,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 				return false;	/* deleted before scan started */
 		}
 
-		if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
+		if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot->shared))
 			return true;
 
 		if (!TransactionIdDidCommit(HeapTupleHeaderGetRawXmax(tuple)))
@@ -1144,7 +1144,7 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 	else
 	{
 		/* xmax is committed, but maybe not according to our snapshot */
-		if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
+		if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot->shared))
 			return true;		/* treat as still in progress */
 	}
 
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index 4bd25e50606..46409cad02d 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -808,7 +808,7 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	/* Finish setting up spgBulkDeleteState */
 	initSpGistState(&bds->spgstate, index);
 	bds->pendingList = NULL;
-	bds->myXmin = GetActiveSnapshot()->mvcc.xmin;
+	bds->myXmin = GetActiveSnapshot()->mvcc.shared->xmin;
 	bds->lastFilledBlock = SPGIST_LAST_FIXED_BLKNO;
 
 	/*
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 231106270fd..81792f0eab3 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -231,7 +231,7 @@ we must ensure consistency about the commit order of transactions.
 For example, suppose an UPDATE in xact A is blocked by xact B's prior
 update of the same row, and xact B is doing commit while xact C gets a
 snapshot.  Xact A can complete and commit as soon as B releases its locks.
-If xact C's GetSnapshotData sees xact B as still running, then it had
+If xact C's GetMVCCSnapshotData sees xact B as still running, then it had
 better see xact A as still running as well, or it will be able to see two
 tuple versions - one deleted by xact B and one inserted by xact A.  Another
 reason why this would be bad is that C would see (in the row inserted by A)
@@ -248,8 +248,8 @@ with snapshot-taking: we do not allow any transaction to exit the set of
 running transactions while a snapshot is being taken.  (This rule is
 stronger than necessary for consistency, but is relatively simple to
 enforce, and it assists with some other issues as explained below.)  The
-implementation of this is that GetSnapshotData takes the ProcArrayLock in
-shared mode (so that multiple backends can take snapshots in parallel),
+implementation of this is that GetMVCCSnapshotData takes the ProcArrayLock
+in shared mode (so that multiple backends can take snapshots in parallel),
 but ProcArrayEndTransaction must take the ProcArrayLock in exclusive mode
 while clearing the ProcGlobal->xids[] entry at transaction end (either
 commit or abort). (To reduce context switching, when multiple transactions
@@ -257,7 +257,7 @@ commit nearly simultaneously, we have one backend take ProcArrayLock and
 clear the XIDs of multiple processes at once.)
 
 ProcArrayEndTransaction also holds the lock while advancing the shared
-latestCompletedXid variable.  This allows GetSnapshotData to use
+latestCompletedXid variable.  This allows GetMVCCSnapshotData to use
 latestCompletedXid + 1 as xmax for its snapshot: there can be no
 transaction >= this xid value that the snapshot needs to consider as
 completed.
@@ -301,7 +301,7 @@ if it currently has no live snapshots (eg, if it's between transactions or
 hasn't yet set a snapshot for a new transaction).  ComputeXidHorizons takes
 the MIN() of the valid xmin fields.  It does this with only shared lock on
 ProcArrayLock, which means there is a potential race condition against other
-backends doing GetSnapshotData concurrently: we must be certain that a
+backends doing GetMVCCSnapshotData concurrently: we must be certain that a
 concurrent backend that is about to set its xmin does not compute an xmin
 less than what ComputeXidHorizons determines.  We ensure that by including
 all the active XIDs into the MIN() calculation, along with the valid xmins.
@@ -310,27 +310,27 @@ ensures that concurrent holders of shared ProcArrayLock will compute the
 same minimum of currently-active XIDs: no xact, in particular not the
 oldest, can exit while we hold shared ProcArrayLock.  So
 ComputeXidHorizons's view of the minimum active XID will be the same as that
-of any concurrent GetSnapshotData, and so it can't produce an overestimate.
+of any concurrent GetMVCCSnapshotData, and so it can't produce an overestimate.
 If there is no active transaction at all, ComputeXidHorizons uses
 latestCompletedXid + 1, which is a lower bound for the xmin that might
-be computed by concurrent or later GetSnapshotData calls.  (We know that no
+be computed by concurrent or later GetMVCCSnapshotData calls.  (We know that no
 XID less than this could be about to appear in the ProcArray, because of the
 XidGenLock interlock discussed above.)
 
-As GetSnapshotData is performance critical, it does not perform an accurate
+As GetMVCCSnapshotData is performance critical, it does not perform an accurate
 oldest-xmin calculation (it used to, until v14). The contents of a snapshot
 only depend on the xids of other backends, not their xmin. As backend's xmin
-changes much more often than its xid, having GetSnapshotData look at xmins
+changes much more often than its xid, having GetMVCCSnapshotData look at xmins
 can lead to a lot of unnecessary cacheline ping-pong.  Instead
-GetSnapshotData updates approximate thresholds (one that guarantees that all
-deleted rows older than it can be removed, another determining that deleted
+GetMVCCSnapshotData updates approximate thresholds (one that guarantees that
+all deleted rows older than it can be removed, another determining that deleted
 rows newer than it can not be removed). GlobalVisTest* uses those thresholds
 to make invisibility decision, falling back to ComputeXidHorizons if
 necessary.
 
 Note that while it is certain that two concurrent executions of
-GetSnapshotData will compute the same xmin for their own snapshots, there is
-no such guarantee for the horizons computed by ComputeXidHorizons.  This is
+GetMVCCSnapshotData will compute the same xmin for their own snapshots, there
+is no such guarantee for the horizons computed by ComputeXidHorizons.  This is
 because we allow XID-less transactions to clear their MyProc->xmin
 asynchronously (without taking ProcArrayLock), so one execution might see
 what had been the oldest xmin, and another not.  This is OK since the
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index b658601bf77..f1148dbe4a3 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -143,12 +143,12 @@ find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
 			if (omit_detached && ActiveSnapshotSet())
 			{
 				TransactionId xmin;
-				Snapshot	snap;
+				MVCCSnapshot snap;
 
 				xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data);
-				snap = GetActiveSnapshot();
+				snap = (MVCCSnapshot) GetActiveSnapshot();
 
-				if (!XidInMVCCSnapshot(xmin, (MVCCSnapshot) snap))
+				if (!XidInMVCCSnapshot(xmin, snap->shared))
 				{
 					if (detached_xmin)
 					{
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index 7c32d28a9b2..cf788c5ba4e 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -2018,7 +2018,7 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 		/* Ignore messages destined for other databases */
 		if (qe->dboid == MyDatabaseId)
 		{
-			if (XidInMVCCSnapshot(qe->xid, (MVCCSnapshot) snapshot))
+			if (XidInMVCCSnapshot(qe->xid, ((MVCCSnapshot) snapshot)->shared))
 			{
 				/*
 				 * The source transaction is still in progress, so we can't
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 76a94172200..cf1603fac78 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1760,7 +1760,7 @@ DefineIndex(Oid tableId,
 	 * they must wait for.  But first, save the snapshot's xmin to use as
 	 * limitXmin for GetCurrentVirtualXIDs().
 	 */
-	limitXmin = snapshot->mvcc.xmin;
+	limitXmin = snapshot->mvcc.shared->xmin;
 
 	PopActiveSnapshot();
 	UnregisterSnapshot(snapshot);
@@ -4190,7 +4190,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 		 * We can now do away with our active snapshot, we still need to save
 		 * the xmin limit to wait for older snapshots.
 		 */
-		limitXmin = snapshot->mvcc.xmin;
+		limitXmin = snapshot->mvcc.shared->xmin;
 
 		PopActiveSnapshot();
 		UnregisterSnapshot(snapshot);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b2d52f610f5..794cf8372af 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -21474,7 +21474,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
 	 * all such queries are complete (otherwise we would present them with an
 	 * inconsistent view of catalogs).
 	 */
-	WaitForOlderSnapshots(snap->mvcc.xmin, false);
+	WaitForOlderSnapshots(snap->mvcc.shared->xmin, false);
 
 	DetachPartitionFinalize(rel, partRel, true, InvalidOid);
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 797d8b1ca1c..dbe3d917b8e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -147,8 +147,8 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	Assert(queryDesc != NULL);
 	Assert(queryDesc->estate == NULL);
 
-	/* caller must ensure the query's snapshot is active */
-	Assert(GetActiveSnapshot() == queryDesc->snapshot);
+	/* ensure the query's snapshot is active */
+	PushActiveSnapshot(queryDesc->snapshot);
 
 	/*
 	 * If the transaction is read-only, we need to check if any writes are
@@ -261,6 +261,8 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	InitPlan(queryDesc, eflags);
 
 	MemoryContextSwitchTo(oldcontext);
+
+	PopActiveSnapshot();
 }
 
 /* ----------------------------------------------------------------
@@ -321,8 +323,8 @@ standard_ExecutorRun(QueryDesc *queryDesc,
 	Assert(estate != NULL);
 	Assert(!(estate->es_top_eflags & EXEC_FLAG_EXPLAIN_ONLY));
 
-	/* caller must ensure the query's snapshot is active */
-	Assert(GetActiveSnapshot() == estate->es_snapshot);
+	/* ensure the query's snapshot is active */
+	PushActiveSnapshot(estate->es_snapshot);
 
 	/*
 	 * Switch into per-query memory context
@@ -386,6 +388,8 @@ standard_ExecutorRun(QueryDesc *queryDesc,
 		InstrStopNode(queryDesc->totaltime, estate->es_processed);
 
 	MemoryContextSwitchTo(oldcontext);
+
+	PopActiveSnapshot();
 }
 
 /* ----------------------------------------------------------------
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 26200c5a3d6..2079569ca7e 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -749,7 +749,8 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate,
 	 * worker, which uses it to set es_snapshot.  Make sure we don't set
 	 * es_snapshot differently in the child.
 	 */
-	Assert(GetActiveSnapshot() == estate->es_snapshot);
+	Assert(((MVCCSnapshot) GetActiveSnapshot())->shared == ((MVCCSnapshot) estate->es_snapshot)->shared);
+	Assert(((MVCCSnapshot) GetActiveSnapshot())->curcid == ((MVCCSnapshot) estate->es_snapshot)->curcid);
 
 	/* Everyone's had a chance to ask for space, so now create the DSM. */
 	InitializeParallelDSM(pcxt);
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index a4a3e2b0ab1..038f0add82a 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -102,7 +102,7 @@ RelationGetPartitionDesc(Relation rel, bool omit_detached)
 		Assert(TransactionIdIsValid(rel->rd_partdesc_nodetached_xmin));
 		activesnap = GetActiveSnapshot();
 
-		if (!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, &activesnap->mvcc))
+		if (!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, activesnap->mvcc.shared))
 			return rel->rd_partdesc_nodetached;
 	}
 
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 2e8ea9e21dd..7d754e9dc8b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -389,6 +389,12 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
  *
  * The snapshot will be usable directly in current transaction or exported
  * for loading in different transaction.
+ *
+ * XXX: The snapshot manager doesn't know anything about the returned
+ * snapshot.  It does not hold back MyProc->xmin, nor is it registered with
+ * any resource owner.  There's also no good way to free it, but leaking it is
+ * acceptable for the current usage where only one snapshot is build for the
+ * whole session.
  */
 MVCCSnapshot
 SnapBuildInitialSnapshot(SnapBuild *builder)
@@ -440,11 +446,14 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	MyProc->xmin = historicsnap->xmin;
 
 	/* allocate in transaction context */
-	mvccsnap = palloc(sizeof(MVCCSnapshotData) + sizeof(TransactionId) * GetMaxSnapshotXidCount());
+	mvccsnap = palloc(sizeof(MVCCSnapshotData));
+	mvccsnap->kind = SNAPSHOT_STATIC;
+	mvccsnap->shared = AllocMVCCSnapshotShared();
+	mvccsnap->shared->refcount = 1;
 	mvccsnap->snapshot_type = SNAPSHOT_MVCC;
-	mvccsnap->xmin = historicsnap->xmin;
-	mvccsnap->xmax = historicsnap->xmax;
-	mvccsnap->xip = (TransactionId *) ((char *) mvccsnap + sizeof(MVCCSnapshotData));
+	mvccsnap->shared->xmin = historicsnap->xmin;
+	mvccsnap->shared->xmax = historicsnap->xmax;
+	mvccsnap->shared->xip = (TransactionId *) ((char *) mvccsnap->shared + sizeof(MVCCSnapshotData));
 
 	/*
 	 * snapbuild.c builds transactions in an "inverted" manner, which means it
@@ -470,23 +479,20 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 						 errmsg("initial slot snapshot too large")));
 
-			mvccsnap->xip[newxcnt++] = xid;
+			mvccsnap->shared->xip[newxcnt++] = xid;
 		}
 
 		TransactionIdAdvance(xid);
 	}
-	mvccsnap->xcnt = newxcnt;
+	mvccsnap->shared->xcnt = newxcnt;
 
 	/* Initialize remaining MVCCSnapshot fields */
-	mvccsnap->subxip = NULL;
-	mvccsnap->subxcnt = 0;
-	mvccsnap->suboverflowed = false;
-	mvccsnap->takenDuringRecovery = false;
-	mvccsnap->copied = true;
+	mvccsnap->shared->subxip = NULL;
+	mvccsnap->shared->subxcnt = 0;
+	mvccsnap->shared->suboverflowed = false;
+	mvccsnap->shared->takenDuringRecovery = false;
+	mvccsnap->shared->snapXactCompletionCount = 0;
 	mvccsnap->curcid = FirstCommandId;
-	mvccsnap->active_count = 0;
-	mvccsnap->regd_count = 0;
-	mvccsnap->snapXactCompletionCount = 0;
 
 	pfree(historicsnap);
 
@@ -528,13 +534,13 @@ SnapBuildExportSnapshot(SnapBuild *builder)
 	 * now that we've built a plain snapshot, make it active and use the
 	 * normal mechanisms for exporting it
 	 */
-	snapname = ExportSnapshot(snap);
+	snapname = ExportSnapshot(snap->shared);
 
 	ereport(LOG,
 			(errmsg_plural("exported logical decoding snapshot: \"%s\" with %u transaction ID",
 						   "exported logical decoding snapshot: \"%s\" with %u transaction IDs",
-						   snap->xcnt,
-						   snapname, snap->xcnt)));
+						   snap->shared->xcnt,
+						   snapname, snap->shared->xcnt)));
 	return snapname;
 }
 
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index ac8a9d33323..92738d3bc13 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -2685,7 +2685,7 @@ ProcessStandbyHSFeedbackMessage(void)
 
 	/*
 	 * Set the WalSender's xmin equal to the standby's requested xmin, so that
-	 * the xmin will be taken into account by GetSnapshotData() /
+	 * the xmin will be taken into account by GetMVCCSnapshotData() /
 	 * ComputeXidHorizons().  This will hold back the removal of dead rows and
 	 * thereby prevent the generation of cleanup conflicts on the standby
 	 * server.
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 29a743ee49c..50e6ee6f998 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -62,6 +62,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
@@ -105,7 +106,7 @@ typedef struct ProcArrayStruct
  * MVCC semantics: If the deleted row's xmax is not considered to be running
  * by anyone, the row can be removed.
  *
- * To avoid slowing down GetSnapshotData(), we don't calculate a precise
+ * To avoid slowing down GetMVCCSnapshotData(), we don't calculate a precise
  * cutoff XID while building a snapshot (looking at the frequently changing
  * xmins scales badly). Instead we compute two boundaries while building the
  * snapshot:
@@ -159,7 +160,7 @@ typedef struct ProcArrayStruct
  *
  * The boundaries are FullTransactionIds instead of TransactionIds to avoid
  * wraparound dangers. There e.g. would otherwise exist no procarray state to
- * prevent maybe_needed to become old enough after the GetSnapshotData()
+ * prevent maybe_needed to become old enough after the GetMVCCSnapshotData()
  * call.
  *
  * The typedef is in the header.
@@ -386,7 +387,7 @@ ProcArrayShmemSize(void)
 	/*
 	 * During Hot Standby processing we have a data structure called
 	 * KnownAssignedXids, created in shared memory. Local data structures are
-	 * also created in various backends during GetSnapshotData(),
+	 * also created in various backends during GetMVCCSnapshotData(),
 	 * TransactionIdIsInProgress() and GetRunningTransactionData(). All of the
 	 * main structures created in those functions must be identically sized,
 	 * since we may at times copy the whole of the data structures around. We
@@ -938,7 +939,7 @@ ProcArrayClearTransaction(PGPROC *proc)
 
 	/*
 	 * Need to increment completion count even though transaction hasn't
-	 * really committed yet. The reason for that is that GetSnapshotData()
+	 * really committed yet. The reason for that is that GetMVCCSnapshotData()
 	 * omits the xid of the current transaction, thus without the increment we
 	 * otherwise could end up reusing the snapshot later. Which would be bad,
 	 * because it might not count the prepared transaction as running.
@@ -2031,7 +2032,7 @@ GetMaxSnapshotSubxidCount(void)
 }
 
 /*
- * Helper function for GetSnapshotData() that checks if the bulk of the
+ * Helper function for GetMVCCSnapshotData() that checks if the bulk of the
  * visibility information in the snapshot is still valid. If so, it updates
  * the fields that need to change and returns true. Otherwise it returns
  * false.
@@ -2040,7 +2041,7 @@ GetMaxSnapshotSubxidCount(void)
  * least in the case we already hold a snapshot), but that's for another day.
  */
 static bool
-GetSnapshotDataReuse(MVCCSnapshot snapshot)
+GetMVCCSnapshotDataReuse(MVCCSnapshotShared snapshot)
 {
 	uint64		curXactCompletionCount;
 
@@ -2060,17 +2061,18 @@ GetSnapshotDataReuse(MVCCSnapshot snapshot)
 	 * contents:
 	 *
 	 * As explained in transam/README, the set of xids considered running by
-	 * GetSnapshotData() cannot change while ProcArrayLock is held. Snapshot
-	 * contents only depend on transactions with xids and xactCompletionCount
-	 * is incremented whenever a transaction with an xid finishes (while
-	 * holding ProcArrayLock exclusively). Thus the xactCompletionCount check
-	 * ensures we would detect if the snapshot would have changed.
+	 * GetMVCCSnapshotData() cannot change while ProcArrayLock is held.
+	 * Snapshot contents only depend on transactions with xids and
+	 * xactCompletionCount is incremented whenever a transaction with an xid
+	 * finishes (while holding ProcArrayLock exclusively). Thus the
+	 * xactCompletionCount check ensures we would detect if the snapshot would
+	 * have changed.
 	 *
 	 * As the snapshot contents are the same as it was before, it is safe to
 	 * re-enter the snapshot's xmin into the PGPROC array. None of the rows
 	 * visible under the snapshot could already have been removed (that'd
 	 * require the set of running transactions to change) and it fulfills the
-	 * requirement that concurrent GetSnapshotData() calls yield the same
+	 * requirement that concurrent GetMVCCSnapshotData() calls yield the same
 	 * xmin.
 	 */
 	if (!TransactionIdIsValid(MyProc->xmin))
@@ -2079,17 +2081,11 @@ GetSnapshotDataReuse(MVCCSnapshot snapshot)
 	RecentXmin = snapshot->xmin;
 	Assert(TransactionIdPrecedesOrEquals(TransactionXmin, RecentXmin));
 
-	snapshot->curcid = GetCurrentCommandId(false);
-	snapshot->active_count = 0;
-	snapshot->regd_count = 0;
-	snapshot->copied = false;
-	snapshot->valid = true;
-
 	return true;
 }
 
 /*
- * GetSnapshotData -- returns information about running transactions.
+ * GetMVCCSnapshotData -- returns information about running transactions.
  *
  * The returned snapshot includes xmin (lowest still-running xact ID),
  * xmax (highest completed xact ID + 1), and a list of running xact IDs
@@ -2116,12 +2112,9 @@ GetSnapshotDataReuse(MVCCSnapshot snapshot)
  *
  * And try to advance the bounds of GlobalVis{Shared,Catalog,Data,Temp}Rels
  * for the benefit of the GlobalVisTest* family of functions.
- *
- * Note: this function should probably not be called with an argument that's
- * not statically allocated (see xip allocation below).
  */
-MVCCSnapshot
-GetSnapshotData(MVCCSnapshot snapshot)
+MVCCSnapshotShared
+GetMVCCSnapshotData(void)
 {
 	ProcArrayStruct *arrayP = procArray;
 	TransactionId *other_xids = ProcGlobal->xids;
@@ -2135,43 +2128,34 @@ GetSnapshotData(MVCCSnapshot snapshot)
 	int			mypgxactoff;
 	TransactionId myxid;
 	uint64		curXactCompletionCount;
+	MVCCSnapshotShared snapshot;
 
 	TransactionId replication_slot_xmin = InvalidTransactionId;
 	TransactionId replication_slot_catalog_xmin = InvalidTransactionId;
 
-	Assert(snapshot != NULL);
-
-	/*
-	 * Allocating space for maxProcs xids is usually overkill; numProcs would
-	 * be sufficient.  But it seems better to do the malloc while not holding
-	 * the lock, so we can't look at numProcs.  Likewise, we allocate much
-	 * more subxip storage than is probably needed.
+	/*---
+	 * Allocate an MVCCSnapshotShared struct.  There are three cases:
+	 *
+	 * 1. No transactions have completed since the last call: we can reuse the
+	 *    latest snapshot information.  See GetMVCCSnapshotDataReuse().
+	 *
+	 * 2. Need to recalculate the snapshot, and 'latestSnapshotShared' is not
+	 *    currently in use by any snapshot.  We can overwrite its contents.
+	 *
+	 * 3. Need to recalculate the XID list and 'latestSnapshotShared' is still
+	 *    in use.  We need to allocate a new MVCCSnapshotShared struct.
 	 *
-	 * This does open a possibility for avoiding repeated malloc/free: since
-	 * maxProcs does not change at runtime, we can simply reuse the previous
-	 * xip arrays if any.  (This relies on the fact that all callers pass
-	 * static SnapshotData structs.)
+	 * We don't know if 'latestSnapshotShared' can be reused before we acquire
+	 * the lock, but if we do need to allocate, we want to do it before
+	 * acquiring the lock.  Therefore, we always make the allocation if we
+	 * might need it and if it turns out to have been unnecessary, we stash
+	 * away the allocated struct in 'spareSnapshotShared' to be reused on next
+	 * call.  This way, the unnecessary allocation is very cheap.
 	 */
-	if (snapshot->xip == NULL)
-	{
-		/*
-		 * First call for this snapshot. Snapshot is same size whether or not
-		 * we are in recovery, see later comments.
-		 */
-		snapshot->xip = (TransactionId *)
-			malloc(GetMaxSnapshotXidCount() * sizeof(TransactionId));
-		if (snapshot->xip == NULL)
-			ereport(ERROR,
-					(errcode(ERRCODE_OUT_OF_MEMORY),
-					 errmsg("out of memory")));
-		Assert(snapshot->subxip == NULL);
-		snapshot->subxip = (TransactionId *)
-			malloc(GetMaxSnapshotSubxidCount() * sizeof(TransactionId));
-		if (snapshot->subxip == NULL)
-			ereport(ERROR,
-					(errcode(ERRCODE_OUT_OF_MEMORY),
-					 errmsg("out of memory")));
-	}
+	if (latestSnapshotShared && latestSnapshotShared->refcount == 0)
+		snapshot = latestSnapshotShared;	/* case 1 or 2 */
+	else
+		snapshot = AllocMVCCSnapshotShared();	/* case 1 or 3 */
 
 	/*
 	 * It is sufficient to get shared lock on ProcArrayLock, even if we are
@@ -2179,10 +2163,14 @@ GetSnapshotData(MVCCSnapshot snapshot)
 	 */
 	LWLockAcquire(ProcArrayLock, LW_SHARED);
 
-	if (GetSnapshotDataReuse(snapshot))
+	if (latestSnapshotShared && GetMVCCSnapshotDataReuse(latestSnapshotShared))
 	{
 		LWLockRelease(ProcArrayLock);
-		return snapshot;
+
+		/* if we made an allocation, stash it away for next call */
+		if (snapshot != latestSnapshotShared)
+			spareSnapshotShared = snapshot;
+		return latestSnapshotShared;
 	}
 
 	latest_completed = TransamVariables->latestCompletedXid;
@@ -2454,16 +2442,18 @@ GetSnapshotData(MVCCSnapshot snapshot)
 	snapshot->suboverflowed = suboverflowed;
 	snapshot->snapXactCompletionCount = curXactCompletionCount;
 
-	snapshot->curcid = GetCurrentCommandId(false);
-
 	/*
-	 * This is a new snapshot, so set both refcounts are zero, and mark it as
-	 * not copied in persistent memory.
+	 * If we allocated a new struct for this, remember that it is the latest
+	 * now and adjust the refcounts accordingly.
 	 */
-	snapshot->active_count = 0;
-	snapshot->regd_count = 0;
-	snapshot->copied = false;
-	snapshot->valid = true;
+	if (snapshot != latestSnapshotShared)
+	{
+		Assert(snapshot->refcount == 0);
+
+		if (latestSnapshotShared && latestSnapshotShared->refcount == 0)
+			FreeMVCCSnapshotShared(latestSnapshotShared);
+		latestSnapshotShared = snapshot;
+	}
 
 	return snapshot;
 }
@@ -2533,10 +2523,10 @@ ProcArrayInstallImportedXmin(TransactionId xmin,
 			continue;
 
 		/*
-		 * We're good.  Install the new xmin.  As in GetSnapshotData, set
+		 * We're good.  Install the new xmin.  As in GetMVCCSnapshotData, set
 		 * TransactionXmin too.  (Note that because snapmgr.c called
-		 * GetSnapshotData first, we'll be overwriting a valid xmin here, so
-		 * we don't check that.)
+		 * GetMVCCSnapshotData first, we'll be overwriting a valid xmin here,
+		 * so we don't check that.)
 		 */
 		MyProc->xmin = TransactionXmin = xmin;
 
@@ -2607,7 +2597,7 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc)
 /*
  * GetRunningTransactionData -- returns information about running transactions.
  *
- * Similar to GetSnapshotData but returns more information. We include
+ * Similar to GetMVCCSnapshotData but returns more information. We include
  * all PGPROCs with an assigned TransactionId, even VACUUM processes and
  * prepared transactions.
  *
@@ -2629,7 +2619,7 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc)
  * entries here to not hold on ProcArrayLock more than necessary.
  *
  * We don't worry about updating other counters, we want to keep this as
- * simple as possible and leave GetSnapshotData() as the primary code for
+ * simple as possible and leave GetMVCCSnapshotData() as the primary code for
  * that bookkeeping.
  *
  * Note that if any transaction has overflowed its cached subtransactions
@@ -2814,7 +2804,7 @@ GetRunningTransactionData(void)
 /*
  * GetOldestActiveTransactionId()
  *
- * Similar to GetSnapshotData but returns just oldestActiveXid. We include
+ * Similar to GetMVCCSnapshotData but returns just oldestActiveXid. We include
  * all PGPROCs with an assigned TransactionId, even VACUUM processes.
  *
  * If allDbs is true, we look at all databases, though there is no need to
@@ -2825,7 +2815,7 @@ GetRunningTransactionData(void)
  * KnownAssignedXids.
  *
  * We don't worry about updating other counters, we want to keep this as
- * simple as possible and leave GetSnapshotData() as the primary code for
+ * simple as possible and leave GetMVCCSnapshotData() as the primary code for
  * that bookkeeping.
  *
  * inCommitOnly indicates getting the oldestActiveXid among the transactions
@@ -4316,7 +4306,7 @@ FullXidRelativeTo(FullTransactionId rel, TransactionId xid)
  * During hot standby we do not fret too much about the distinction between
  * top-level XIDs and subtransaction XIDs. We store both together in the
  * KnownAssignedXids list.  In backends, this is copied into snapshots in
- * GetSnapshotData(), taking advantage of the fact that XidInMVCCSnapshot()
+ * GetMVCCSnapshotData(), taking advantage of the fact that XidInMVCCSnapshot()
  * doesn't care about the distinction either.  Subtransaction XIDs are
  * effectively treated as top-level XIDs and in the typical case pg_subtrans
  * links are *not* maintained (which does not affect visibility).
diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index 8d97127668c..b381f5605e0 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -449,10 +449,10 @@ static void SerialSetActiveSerXmin(TransactionId xid);
 
 static uint32 predicatelock_hash(const void *key, Size keysize);
 static void SummarizeOldestCommittedSxact(void);
-static MVCCSnapshot GetSafeSnapshot(MVCCSnapshot origSnapshot);
-static MVCCSnapshot GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
-														  VirtualTransactionId *sourcevxid,
-														  int sourcepid);
+static MVCCSnapshotShared GetSafeSnapshot(void);
+static MVCCSnapshotShared GetSerializableTransactionSnapshotInt(VirtualTransactionId *sourcevxid,
+																TransactionId sourcexmin,
+																int sourcepid);
 static bool PredicateLockExists(const PREDICATELOCKTARGETTAG *targettag);
 static bool GetParentPredicateLockTag(const PREDICATELOCKTARGETTAG *tag,
 									  PREDICATELOCKTARGETTAG *parent);
@@ -1550,25 +1550,20 @@ SummarizeOldestCommittedSxact(void)
  *
  *		As with GetSerializableTransactionSnapshot (which this is a subroutine
  *		for), the passed-in Snapshot pointer should reference a static data
- *		area that can safely be passed to GetSnapshotData.
+ *		area that can safely be passed to GetMVCCSnapshotData.
  */
-static MVCCSnapshot
-GetSafeSnapshot(MVCCSnapshot origSnapshot)
+static MVCCSnapshotShared
+GetSafeSnapshot(void)
 {
-	MVCCSnapshot snapshot;
+	MVCCSnapshotShared snapshot;
 
 	Assert(XactReadOnly && XactDeferrable);
 
 	while (true)
 	{
-		/*
-		 * GetSerializableTransactionSnapshotInt is going to call
-		 * GetSnapshotData, so we need to provide it the static snapshot area
-		 * our caller passed to us.  The pointer returned is actually the same
-		 * one passed to it, but we avoid assuming that here.
-		 */
-		snapshot = GetSerializableTransactionSnapshotInt(origSnapshot,
-														 NULL, InvalidPid);
+		snapshot = GetSerializableTransactionSnapshotInt(NULL,
+														 InvalidTransactionId,
+														 InvalidPid);
 
 		if (MySerializableXact == InvalidSerializableXact)
 			return snapshot;	/* no concurrent r/w xacts; it's safe */
@@ -1671,13 +1666,11 @@ GetSafeSnapshotBlockingPids(int blocked_pid, int *output, int output_size)
  * Make sure we have a SERIALIZABLEXACT reference in MySerializableXact.
  * It should be current for this process and be contained in PredXact.
  *
- * The passed-in Snapshot pointer should reference a static data area that
- * can safely be passed to GetSnapshotData.  The return value is actually
- * always this same pointer; no new snapshot data structure is allocated
- * within this function.
+ * This calls GetMVCCSnapshotData to do the heavy lifting, but also sets up
+ * shared memory data structures specific to serializable transactions.
  */
-MVCCSnapshot
-GetSerializableTransactionSnapshot(MVCCSnapshot snapshot)
+MVCCSnapshotShared
+GetSerializableTransactionSnapshotData(void)
 {
 	Assert(IsolationIsSerializable());
 
@@ -1700,26 +1693,25 @@ GetSerializableTransactionSnapshot(MVCCSnapshot snapshot)
 	 * thereby avoid all SSI overhead once it's running.
 	 */
 	if (XactReadOnly && XactDeferrable)
-		return GetSafeSnapshot(snapshot);
+		return GetSafeSnapshot();
 
-	return GetSerializableTransactionSnapshotInt(snapshot,
-												 NULL, InvalidPid);
+	return GetSerializableTransactionSnapshotInt(NULL, InvalidTransactionId, InvalidPid);
 }
 
 /*
  * Import a snapshot to be used for the current transaction.
  *
- * This is nearly the same as GetSerializableTransactionSnapshot, except that
- * we don't take a new snapshot, but rather use the data we're handed.
+ * This is nearly the same as GetSerializableTransactionSnapshotData, except
+ * that we don't take a new snapshot, but rather use the data we're handed.
  *
  * The caller must have verified that the snapshot came from a serializable
  * transaction; and if we're read-write, the source transaction must not be
  * read-only.
  */
 void
-SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
-								   VirtualTransactionId *sourcevxid,
-								   int sourcepid)
+SetSerializableTransactionSnapshotData(MVCCSnapshotShared snapshot,
+									   VirtualTransactionId *sourcevxid,
+									   int sourcepid)
 {
 	Assert(IsolationIsSerializable());
 
@@ -1745,28 +1737,29 @@ SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("a snapshot-importing transaction must not be READ ONLY DEFERRABLE")));
 
-	(void) GetSerializableTransactionSnapshotInt(snapshot, sourcevxid,
-												 sourcepid);
+	(void) GetSerializableTransactionSnapshotInt(sourcevxid, snapshot->xmin, sourcepid);
 }
 
 /*
  * Guts of GetSerializableTransactionSnapshot
  *
  * If sourcevxid is valid, this is actually an import operation and we should
- * skip calling GetSnapshotData, because the snapshot contents are already
+ * skip calling GetMVCCSnapshotData, because the snapshot contents are already
  * loaded up.  HOWEVER: to avoid race conditions, we must check that the
  * source xact is still running after we acquire SerializableXactHashLock.
  * We do that by calling ProcArrayInstallImportedXmin.
  */
-static MVCCSnapshot
-GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
-									  VirtualTransactionId *sourcevxid,
+static MVCCSnapshotShared
+GetSerializableTransactionSnapshotInt(VirtualTransactionId *sourcevxid,
+									  TransactionId sourcexmin,
 									  int sourcepid)
 {
 	PGPROC	   *proc;
 	VirtualTransactionId vxid;
 	SERIALIZABLEXACT *sxact,
 			   *othersxact;
+	MVCCSnapshotShared snapshot;
+	TransactionId xmin;
 
 	/* We only do this for serializable transactions.  Once. */
 	Assert(MySerializableXact == InvalidSerializableXact);
@@ -1791,7 +1784,7 @@ GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 	 *
 	 * We must hold SerializableXactHashLock when taking/checking the snapshot
 	 * to avoid race conditions, for much the same reasons that
-	 * GetSnapshotData takes the ProcArrayLock.  Since we might have to
+	 * GetMVCCSnapshotData takes the ProcArrayLock.  Since we might have to
 	 * release SerializableXactHashLock to call SummarizeOldestCommittedSxact,
 	 * this means we have to create the sxact first, which is a bit annoying
 	 * (in particular, an elog(ERROR) in procarray.c would cause us to leak
@@ -1815,16 +1808,24 @@ GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 
 	/* Get the snapshot, or check that it's safe to use */
 	if (!sourcevxid)
-		snapshot = GetSnapshotData(snapshot);
-	else if (!ProcArrayInstallImportedXmin(snapshot->xmin, sourcevxid))
 	{
-		ReleasePredXact(sxact);
-		LWLockRelease(SerializableXactHashLock);
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("could not import the requested snapshot"),
-				 errdetail("The source process with PID %d is not running anymore.",
-						   sourcepid)));
+		snapshot = GetMVCCSnapshotData();
+		xmin = snapshot->xmin;
+	}
+	else
+	{
+		if (!ProcArrayInstallImportedXmin(sourcexmin, sourcevxid))
+		{
+			ReleasePredXact(sxact);
+			LWLockRelease(SerializableXactHashLock);
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg("could not import the requested snapshot"),
+					 errdetail("The source process with PID %d is not running anymore.",
+							   sourcepid)));
+		}
+		snapshot = NULL;
+		xmin = sourcexmin;
 	}
 
 	/*
@@ -1856,7 +1857,7 @@ GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 	dlist_init(&(sxact->possibleUnsafeConflicts));
 	sxact->topXid = GetTopTransactionIdIfAny();
 	sxact->finishedBefore = InvalidTransactionId;
-	sxact->xmin = snapshot->xmin;
+	sxact->xmin = xmin;
 	sxact->pid = MyProcPid;
 	sxact->pgprocno = MyProcNumber;
 	dlist_init(&sxact->predicateLocks);
@@ -1910,18 +1911,18 @@ GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 	if (!TransactionIdIsValid(PredXact->SxactGlobalXmin))
 	{
 		Assert(PredXact->SxactGlobalXminCount == 0);
-		PredXact->SxactGlobalXmin = snapshot->xmin;
+		PredXact->SxactGlobalXmin = xmin;
 		PredXact->SxactGlobalXminCount = 1;
-		SerialSetActiveSerXmin(snapshot->xmin);
+		SerialSetActiveSerXmin(xmin);
 	}
-	else if (TransactionIdEquals(snapshot->xmin, PredXact->SxactGlobalXmin))
+	else if (TransactionIdEquals(xmin, PredXact->SxactGlobalXmin))
 	{
 		Assert(PredXact->SxactGlobalXminCount > 0);
 		PredXact->SxactGlobalXminCount++;
 	}
 	else
 	{
-		Assert(TransactionIdFollows(snapshot->xmin, PredXact->SxactGlobalXmin));
+		Assert(TransactionIdFollows(xmin, PredXact->SxactGlobalXmin));
 	}
 
 	MySerializableXact = sxact;
@@ -3976,13 +3977,13 @@ XidIsConcurrent(TransactionId xid)
 
 	snap = (MVCCSnapshot) GetTransactionSnapshot();
 
-	if (TransactionIdPrecedes(xid, snap->xmin))
+	if (TransactionIdPrecedes(xid, snap->shared->xmin))
 		return false;
 
-	if (TransactionIdFollowsOrEquals(xid, snap->xmax))
+	if (TransactionIdFollowsOrEquals(xid, snap->shared->xmax))
 		return true;
 
-	return pg_lfind32(xid, snap->xip, snap->xcnt);
+	return pg_lfind32(xid, snap->shared->xip, snap->shared->xcnt);
 }
 
 bool
diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index 0407d16fe12..fe9b521575e 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -381,7 +381,7 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 		elog(ERROR, "no active snapshot set");
 
 	/* allocate */
-	nxip = cur->xcnt;
+	nxip = cur->shared->xcnt;
 	snap = palloc(PG_SNAPSHOT_SIZE(nxip));
 
 	/*
@@ -390,12 +390,12 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	 * advance past any of these XIDs.  Hence, these XIDs remain allowable
 	 * relative to next_fxid.
 	 */
-	snap->xmin = FullTransactionIdFromAllowableAt(next_fxid, cur->xmin);
-	snap->xmax = FullTransactionIdFromAllowableAt(next_fxid, cur->xmax);
+	snap->xmin = FullTransactionIdFromAllowableAt(next_fxid, cur->shared->xmin);
+	snap->xmax = FullTransactionIdFromAllowableAt(next_fxid, cur->shared->xmax);
 	snap->nxip = nxip;
 	for (i = 0; i < nxip; i++)
 		snap->xip[i] =
-			FullTransactionIdFromAllowableAt(next_fxid, cur->xip[i]);
+			FullTransactionIdFromAllowableAt(next_fxid, cur->shared->xip[i]);
 
 	/*
 	 * We want them guaranteed to be in ascending order.  This also removes
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 0ff8cf9bf5f..dd0c41e579d 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -123,9 +123,6 @@
  * special-purpose code (say, RI checking.)  CatalogSnapshot points to an
  * MVCC snapshot intended to be used for catalog scans; we must invalidate it
  * whenever a system catalog change occurs.
- *
- * These SnapshotData structs are static to simplify memory allocation
- * (see the hack in GetSnapshotData to avoid repeated malloc/free).
  */
 static MVCCSnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
 static MVCCSnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
@@ -138,7 +135,7 @@ SnapshotData SnapshotToastData = {SNAPSHOT_TOAST};
 static HistoricMVCCSnapshot HistoricSnapshot = NULL;
 
 /*
- * These are updated by GetSnapshotData.  We initialize them this way
+ * These are updated by GetMVCCSnapshotData.  We initialize them this way
  * for the convenience of TransactionIdIsInProgress: even in bootstrap
  * mode, we don't want it to say that BootstrapTransactionId is in progress.
  */
@@ -151,14 +148,12 @@ static HTAB *tuplecid_data = NULL;
 /*
  * Elements of the active snapshot stack.
  *
- * Each element here accounts for exactly one active_count on SnapshotData.
- *
  * NB: the code assumes that elements in this list are in non-increasing
  * order of as_level; also, the list must be NULL-terminated.
  */
 typedef struct ActiveSnapshotElt
 {
-	MVCCSnapshot as_snap;
+	MVCCSnapshotData as_snap;
 	int			as_level;
 	struct ActiveSnapshotElt *as_next;
 } ActiveSnapshotElt;
@@ -190,19 +185,23 @@ static bool FirstXactSnapshotRegistered = false;
 typedef struct ExportedSnapshot
 {
 	char	   *snapfile;
-	MVCCSnapshot snapshot;
+	MVCCSnapshotShared snapshot;
 } ExportedSnapshot;
 
 /* Current xact's exported snapshots (a list of ExportedSnapshot structs) */
 static List *exportedSnapshots = NIL;
 
+MVCCSnapshotShared latestSnapshotShared = NULL;
+MVCCSnapshotShared spareSnapshotShared = NULL;
+
 /* Prototypes for local functions */
-static MVCCSnapshot CopyMVCCSnapshot(MVCCSnapshot snapshot);
+static void UpdateStaticMVCCSnapshot(MVCCSnapshot snapshot, MVCCSnapshotShared shared);
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
-static void FreeMVCCSnapshot(MVCCSnapshot snapshot);
 static void SnapshotResetXmin(void);
-static void valid_snapshots_push_tail(MVCCSnapshot snapshot);
-static void valid_snapshots_push_out_of_order(MVCCSnapshot snapshot);
+static void ReleaseMVCCSnapshotShared(MVCCSnapshotShared shared);
+static void valid_snapshots_push_tail(MVCCSnapshotShared snapshot);
+static void valid_snapshots_push_out_of_order(MVCCSnapshotShared snapshot);
+
 
 /* ResourceOwner callbacks to track snapshot references */
 static void ResOwnerReleaseSnapshot(Datum res);
@@ -279,6 +278,8 @@ GetTransactionSnapshot(void)
 	/* First call in transaction? */
 	if (!FirstSnapshotSet)
 	{
+		MVCCSnapshotShared shared;
+
 		/*
 		 * Don't allow catalog snapshot to be older than xact snapshot.  Must
 		 * do this first to allow the empty-heap Assert to succeed.
@@ -300,23 +301,18 @@ GetTransactionSnapshot(void)
 		 * mode, predicate.c needs to wrap the snapshot fetch in its own
 		 * processing.
 		 */
+		if (IsolationIsSerializable())
+			shared = GetSerializableTransactionSnapshotData();
+		else
+			shared = GetMVCCSnapshotData();
+
+		UpdateStaticMVCCSnapshot(&CurrentSnapshotData, shared);
+
 		if (IsolationUsesXactSnapshot())
 		{
-			/* First, create the snapshot in CurrentSnapshotData */
-			if (IsolationIsSerializable())
-				GetSerializableTransactionSnapshot(&CurrentSnapshotData);
-			else
-				GetSnapshotData(&CurrentSnapshotData);
-
-			/* Mark it as "registered" */
+			/* keep it */
 			FirstXactSnapshotRegistered = true;
 		}
-		else
-		{
-			GetSnapshotData(&CurrentSnapshotData);
-		}
-		valid_snapshots_push_tail(&CurrentSnapshotData);
-
 		FirstSnapshotSet = true;
 		return (Snapshot) &CurrentSnapshotData;
 	}
@@ -331,14 +327,31 @@ GetTransactionSnapshot(void)
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
-	if (CurrentSnapshotData.valid)
-		dlist_delete(&CurrentSnapshotData.node);
-	GetSnapshotData(&CurrentSnapshotData);
-	valid_snapshots_push_tail(&CurrentSnapshotData);
-
+	UpdateStaticMVCCSnapshot(&CurrentSnapshotData, GetMVCCSnapshotData());
 	return (Snapshot) &CurrentSnapshotData;
 }
 
+/*
+ * Update a static snapshot with the given shared struct.
+ *
+ * If the static snapshot is previously valid, release its old 'shared'
+ * struct first.
+ */
+static void
+UpdateStaticMVCCSnapshot(MVCCSnapshot snapshot, MVCCSnapshotShared shared)
+{
+	/* Replace the 'shared' struct */
+	if (snapshot->shared)
+		ReleaseMVCCSnapshotShared(snapshot->shared);
+	snapshot->shared = shared;
+	snapshot->shared->refcount++;
+	if (snapshot->shared->refcount == 1)
+		valid_snapshots_push_tail(shared);
+
+	snapshot->curcid = GetCurrentCommandId(false);
+	snapshot->valid = true;
+}
+
 /*
  * GetLatestSnapshot
  *		Get a snapshot that is up-to-date as of the current instant,
@@ -365,10 +378,7 @@ GetLatestSnapshot(void)
 	if (!FirstSnapshotSet)
 		return GetTransactionSnapshot();
 
-	if (SecondarySnapshotData.valid)
-		dlist_delete(&SecondarySnapshotData.node);
-	GetSnapshotData(&SecondarySnapshotData);
-	valid_snapshots_push_tail(&SecondarySnapshotData);
+	UpdateStaticMVCCSnapshot(&SecondarySnapshotData, GetMVCCSnapshotData());
 
 	return (Snapshot) &SecondarySnapshotData;
 }
@@ -418,7 +428,7 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 	if (!CatalogSnapshotData.valid)
 	{
 		/* Get new snapshot. */
-		GetSnapshotData(&CatalogSnapshotData);
+		UpdateStaticMVCCSnapshot(&CatalogSnapshotData, GetMVCCSnapshotData());
 
 		/*
 		 * Make sure the catalog snapshot will be accounted for in decisions
@@ -432,7 +442,6 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		 * NB: it had better be impossible for this to throw error, since the
 		 * CatalogSnapshot pointer is already valid.
 		 */
-		valid_snapshots_push_tail(&CatalogSnapshotData);
 	}
 
 	return (Snapshot) &CatalogSnapshotData;
@@ -453,18 +462,21 @@ InvalidateCatalogSnapshot(void)
 {
 	if (CatalogSnapshotData.valid)
 	{
-		dlist_delete(&CatalogSnapshotData.node);
+		ReleaseMVCCSnapshotShared(CatalogSnapshotData.shared);
+		CatalogSnapshotData.shared = NULL;
 		CatalogSnapshotData.valid = false;
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
 	}
 	if (!FirstXactSnapshotRegistered && CurrentSnapshotData.valid)
 	{
-		dlist_delete(&CurrentSnapshotData.node);
+		ReleaseMVCCSnapshotShared(CurrentSnapshotData.shared);
+		CurrentSnapshotData.shared = NULL;
 		CurrentSnapshotData.valid = false;
 	}
 	if (SecondarySnapshotData.valid)
 	{
-		dlist_delete(&SecondarySnapshotData.node);
+		ReleaseMVCCSnapshotShared(SecondarySnapshotData.shared);
+		SecondarySnapshotData.shared = NULL;
 		SecondarySnapshotData.valid = false;
 	}
 
@@ -479,13 +491,14 @@ InvalidateCatalogSnapshot(void)
  * want to continue holding the catalog snapshot if it might mean that the
  * global xmin horizon can't advance.  However, if there are other snapshots
  * still active or registered, the catalog snapshot isn't likely to be the
- * oldest one, so we might as well keep it.
+ * oldest one, so we might as well keep it. XXX
  */
 void
 InvalidateCatalogSnapshotConditionally(void)
 {
 	if (CatalogSnapshotData.valid &&
-		dlist_head_node(&ValidSnapshots) == &CatalogSnapshotData.node)
+		dlist_tail_node(&ValidSnapshots) == &CatalogSnapshotData.shared->node &&
+		CatalogSnapshotData.shared->refcount == 1)
 		InvalidateCatalogSnapshot();
 }
 
@@ -515,7 +528,7 @@ SnapshotSetCommandId(CommandId curcid)
  * in GetTransactionSnapshot.
  */
 static void
-SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid,
+SetTransactionSnapshot(MVCCSnapshotShared sourcesnap, VirtualTransactionId *sourcevxid,
 					   int sourcepid, PGPROC *sourceproc)
 {
 	/* Caller should have checked this already */
@@ -526,38 +539,25 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 
 	Assert(!FirstXactSnapshotRegistered);
 	Assert(!HistoricSnapshotActive());
+	Assert(sourcesnap->refcount > 0);
 
 	/*
 	 * Even though we are not going to use the snapshot it computes, we must
-	 * call GetSnapshotData, for two reasons: (1) to be sure that
-	 * CurrentSnapshotData's XID arrays have been allocated, and (2) to update
-	 * the state for GlobalVis*.
+	 * call GetMVCCSnapshotData to update the state for GlobalVis*.
 	 */
-	GetSnapshotData(&CurrentSnapshotData);
+	UpdateStaticMVCCSnapshot(&CurrentSnapshotData, GetMVCCSnapshotData());
 
 	/*
 	 * Now copy appropriate fields from the source snapshot.
 	 */
-	CurrentSnapshotData.xmin = sourcesnap->xmin;
-	CurrentSnapshotData.xmax = sourcesnap->xmax;
-	CurrentSnapshotData.xcnt = sourcesnap->xcnt;
-	Assert(sourcesnap->xcnt <= GetMaxSnapshotXidCount());
-	if (sourcesnap->xcnt > 0)
-		memcpy(CurrentSnapshotData.xip, sourcesnap->xip,
-			   sourcesnap->xcnt * sizeof(TransactionId));
-	CurrentSnapshotData.subxcnt = sourcesnap->subxcnt;
-	Assert(sourcesnap->subxcnt <= GetMaxSnapshotSubxidCount());
-	if (sourcesnap->subxcnt > 0)
-		memcpy(CurrentSnapshotData.subxip, sourcesnap->subxip,
-			   sourcesnap->subxcnt * sizeof(TransactionId));
-	CurrentSnapshotData.suboverflowed = sourcesnap->suboverflowed;
-	CurrentSnapshotData.takenDuringRecovery = sourcesnap->takenDuringRecovery;
-	/* NB: curcid should NOT be copied, it's a local matter */
+	ReleaseMVCCSnapshotShared(CurrentSnapshotData.shared);
+	CurrentSnapshotData.shared = sourcesnap;
+	CurrentSnapshotData.shared->refcount++;
 
-	CurrentSnapshotData.snapXactCompletionCount = 0;
+	/* NB: curcid should NOT be copied, it's a local matter */
 
 	/*
-	 * Now we have to fix what GetSnapshotData did with MyProc->xmin and
+	 * Now we have to fix what GetMVCCSnapshotData did with MyProc->xmin and
 	 * TransactionXmin.  There is a race condition: to make sure we are not
 	 * causing the global xmin to go backwards, we have to test that the
 	 * source transaction is still running, and that has to be done
@@ -569,13 +569,13 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	 */
 	if (sourceproc != NULL)
 	{
-		if (!ProcArrayInstallRestoredXmin(CurrentSnapshotData.xmin, sourceproc))
+		if (!ProcArrayInstallRestoredXmin(CurrentSnapshotData.shared->xmin, sourceproc))
 			ereport(ERROR,
 					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 					 errmsg("could not import the requested snapshot"),
 					 errdetail("The source transaction is not running anymore.")));
 	}
-	else if (!ProcArrayInstallImportedXmin(CurrentSnapshotData.xmin, sourcevxid))
+	else if (!ProcArrayInstallImportedXmin(CurrentSnapshotData.shared->xmin, sourcevxid))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("could not import the requested snapshot"),
@@ -591,96 +591,22 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	if (IsolationUsesXactSnapshot())
 	{
 		if (IsolationIsSerializable())
-			SetSerializableTransactionSnapshot(&CurrentSnapshotData, sourcevxid,
-											   sourcepid);
-		/* Mark it as "registered" */
+			SetSerializableTransactionSnapshotData(CurrentSnapshotData.shared,
+												   sourcevxid, sourcepid);
+		/* keep it */
 		FirstXactSnapshotRegistered = true;
 	}
-	valid_snapshots_push_tail(&CurrentSnapshotData);
 
 	FirstSnapshotSet = true;
 }
 
-/*
- * CopyMVCCSnapshot
- *		Copy the given snapshot.
- *
- * The copy is palloc'd in TopTransactionContext and has initial refcounts set
- * to 0.  The returned snapshot has the copied flag set.
- */
-static MVCCSnapshot
-CopyMVCCSnapshot(MVCCSnapshot snapshot)
-{
-	MVCCSnapshot newsnap;
-	Size		subxipoff;
-	Size		size;
-
-	/* We allocate any XID arrays needed in the same palloc block. */
-	size = subxipoff = sizeof(MVCCSnapshotData) +
-		snapshot->xcnt * sizeof(TransactionId);
-	if (snapshot->subxcnt > 0)
-		size += snapshot->subxcnt * sizeof(TransactionId);
-
-	newsnap = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
-	memcpy(newsnap, snapshot, sizeof(MVCCSnapshotData));
-
-	newsnap->regd_count = 0;
-	newsnap->active_count = 0;
-	newsnap->copied = true;
-	newsnap->valid = true;
-	newsnap->snapXactCompletionCount = 0;
-
-	/* setup XID array */
-	if (snapshot->xcnt > 0)
-	{
-		newsnap->xip = (TransactionId *) (newsnap + 1);
-		memcpy(newsnap->xip, snapshot->xip,
-			   snapshot->xcnt * sizeof(TransactionId));
-	}
-	else
-		newsnap->xip = NULL;
-
-	/*
-	 * Setup subXID array. Don't bother to copy it if it had overflowed,
-	 * though, because it's not used anywhere in that case. Except if it's a
-	 * snapshot taken during recovery; all the top-level XIDs are in subxip as
-	 * well in that case, so we mustn't lose them.
-	 */
-	if (snapshot->subxcnt > 0 &&
-		(!snapshot->suboverflowed || snapshot->takenDuringRecovery))
-	{
-		newsnap->subxip = (TransactionId *) ((char *) newsnap + subxipoff);
-		memcpy(newsnap->subxip, snapshot->subxip,
-			   snapshot->subxcnt * sizeof(TransactionId));
-	}
-	else
-		newsnap->subxip = NULL;
-
-	return newsnap;
-}
-
-/*
- * FreeMVCCSnapshot
- *		Free the memory associated with a snapshot.
- */
-static void
-FreeMVCCSnapshot(MVCCSnapshot snapshot)
-{
-	Assert(snapshot->regd_count == 0);
-	Assert(snapshot->active_count == 0);
-	Assert(snapshot->copied);
-	Assert(snapshot->valid);
-
-	pfree(snapshot);
-}
-
 /*
  * PushActiveSnapshot
  *		Set the given snapshot as the current active snapshot
  *
  * If the passed snapshot is a statically-allocated one, or it is possibly
  * subject to a future command counter update, create a new long-lived copy
- * with active refcount=1.  Otherwise, only increment the refcount.
+ * with active refcount=1.  Otherwise, only increment the refcount. XXX
  *
  * Only regular MVCC snaphots can be used as the active snapshot.
  */
@@ -711,20 +637,13 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 	Assert(ActiveSnapshot == NULL || snap_level >= ActiveSnapshot->as_level);
 
 	newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
-
-	if (!origsnap->copied)
-	{
-		newactive->as_snap = CopyMVCCSnapshot(origsnap);
-		dlist_insert_after(&origsnap->node, &newactive->as_snap->node);
-	}
-	else
-		newactive->as_snap = origsnap;
+	memcpy(&newactive->as_snap, origsnap, sizeof(MVCCSnapshotData));
+	newactive->as_snap.kind = SNAPSHOT_ACTIVE;
+	newactive->as_snap.shared->refcount++;
 
 	newactive->as_next = ActiveSnapshot;
 	newactive->as_level = snap_level;
 
-	newactive->as_snap->active_count++;
-
 	ActiveSnapshot = newactive;
 }
 
@@ -739,20 +658,20 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 void
 PushCopiedSnapshot(Snapshot snapshot)
 {
-	MVCCSnapshot copy;
-
 	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
 
-	copy = CopyMVCCSnapshot(&snapshot->mvcc);
-	dlist_insert_after(&snapshot->mvcc.node, &copy->node);
-	PushActiveSnapshot((Snapshot) copy);
+	/*
+	 * This used to be different from PushActiveSnapshot, but these days
+	 * PushActiveSnapshot creates a copy too and there's no difference.
+	 */
+	PushActiveSnapshot(snapshot);
 }
 
 /*
  * UpdateActiveSnapshotCommandId
  *
  * Update the current CID of the active snapshot.  This can only be applied
- * to a snapshot that is not referenced elsewhere.
+ * to a snapshot that is not referenced elsewhere. XXX
  */
 void
 UpdateActiveSnapshotCommandId(void)
@@ -761,8 +680,6 @@ UpdateActiveSnapshotCommandId(void)
 				curcid;
 
 	Assert(ActiveSnapshot != NULL);
-	Assert(ActiveSnapshot->as_snap->active_count == 1);
-	Assert(ActiveSnapshot->as_snap->regd_count == 0);
 
 	/*
 	 * Don't allow modification of the active snapshot during parallel
@@ -772,11 +689,12 @@ UpdateActiveSnapshotCommandId(void)
 	 * CommandCounterIncrement, but there are a few places that call this
 	 * directly, so we put an additional guard here.
 	 */
-	save_curcid = ActiveSnapshot->as_snap->curcid;
+	save_curcid = ActiveSnapshot->as_snap.curcid;
 	curcid = GetCurrentCommandId(false);
 	if (IsInParallelMode() && save_curcid != curcid)
 		elog(ERROR, "cannot modify commandid in active snapshot during a parallel operation");
-	ActiveSnapshot->as_snap->curcid = curcid;
+
+	ActiveSnapshot->as_snap.curcid = curcid;
 }
 
 /*
@@ -792,16 +710,7 @@ PopActiveSnapshot(void)
 
 	newstack = ActiveSnapshot->as_next;
 
-	Assert(ActiveSnapshot->as_snap->active_count > 0);
-
-	ActiveSnapshot->as_snap->active_count--;
-
-	if (ActiveSnapshot->as_snap->active_count == 0 &&
-		ActiveSnapshot->as_snap->regd_count == 0)
-	{
-		dlist_delete(&ActiveSnapshot->as_snap->node);
-		FreeMVCCSnapshot(ActiveSnapshot->as_snap);
-	}
+	ReleaseMVCCSnapshotShared(ActiveSnapshot->as_snap.shared);
 
 	pfree(ActiveSnapshot);
 	ActiveSnapshot = newstack;
@@ -818,7 +727,7 @@ GetActiveSnapshot(void)
 {
 	Assert(ActiveSnapshot != NULL);
 
-	return (Snapshot) ActiveSnapshot->as_snap;
+	return (Snapshot) &ActiveSnapshot->as_snap;
 }
 
 /*
@@ -854,7 +763,7 @@ RegisterSnapshot(Snapshot snapshot)
 Snapshot
 RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 {
-	MVCCSnapshot snapshot;
+	MVCCSnapshot newsnap;
 
 	if (orig_snapshot == InvalidSnapshot)
 		return InvalidSnapshot;
@@ -871,22 +780,19 @@ RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 	}
 
 	Assert(orig_snapshot->snapshot_type == SNAPSHOT_MVCC);
-	snapshot = &orig_snapshot->mvcc;
-	Assert(snapshot->valid);
+	Assert(orig_snapshot->mvcc.valid);
 
-	/* Static snapshot?  Create a persistent copy */
-	if (!snapshot->copied)
-	{
-		snapshot = CopyMVCCSnapshot(snapshot);
-		dlist_insert_after(&orig_snapshot->mvcc.node, &snapshot->node);
-	}
+	/* Create a copy */
+	newsnap = MemoryContextAlloc(TopTransactionContext, sizeof(MVCCSnapshotData));
+	memcpy(newsnap, &orig_snapshot->mvcc, sizeof(MVCCSnapshotData));
+	newsnap->kind = SNAPSHOT_REGISTERED;
+	newsnap->shared->refcount++;
 
 	/* and tell resowner.c about it */
 	ResourceOwnerEnlarge(owner);
-	snapshot->regd_count++;
-	ResourceOwnerRememberSnapshot(owner, (Snapshot) snapshot);
+	ResourceOwnerRememberSnapshot(owner, (Snapshot) newsnap);
 
-	return (Snapshot) snapshot;
+	return (Snapshot) newsnap;
 }
 
 /*
@@ -924,18 +830,12 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 {
 	if (snapshot->snapshot_type == SNAPSHOT_MVCC)
 	{
-		MVCCSnapshot mvccsnap = &snapshot->mvcc;
-
-		Assert(mvccsnap->regd_count > 0);
+		Assert(snapshot->mvcc.kind == SNAPSHOT_REGISTERED);
 		Assert(!dlist_is_empty(&ValidSnapshots));
 
-		mvccsnap->regd_count--;
-		if (mvccsnap->regd_count == 0 && mvccsnap->active_count == 0)
-		{
-			dlist_delete(&mvccsnap->node);
-			FreeMVCCSnapshot(mvccsnap);
-			SnapshotResetXmin();
-		}
+		ReleaseMVCCSnapshotShared(snapshot->mvcc.shared);
+		pfree(snapshot);
+		SnapshotResetXmin();
 	}
 	else if (snapshot->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
 	{
@@ -973,19 +873,21 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 static void
 SnapshotResetXmin(void)
 {
-	MVCCSnapshot minSnapshot;
+	MVCCSnapshotShared minSnapshot;
 
 	/*
 	 * Invalidate these static snapshots so that we can advance xmin.
 	 */
 	if (!FirstXactSnapshotRegistered && CurrentSnapshotData.valid)
 	{
-		dlist_delete(&CurrentSnapshotData.node);
+		ReleaseMVCCSnapshotShared(CurrentSnapshotData.shared);
+		CurrentSnapshotData.shared = NULL;
 		CurrentSnapshotData.valid = false;
 	}
 	if (SecondarySnapshotData.valid)
 	{
-		dlist_delete(&SecondarySnapshotData.node);
+		ReleaseMVCCSnapshotShared(SecondarySnapshotData.shared);
+		SecondarySnapshotData.shared = NULL;
 		SecondarySnapshotData.valid = false;
 	}
 
@@ -998,7 +900,7 @@ SnapshotResetXmin(void)
 		return;
 	}
 
-	minSnapshot = dlist_head_element(MVCCSnapshotData, node, &ValidSnapshots);
+	minSnapshot = dlist_head_element(MVCCSnapshotSharedData, node, &ValidSnapshots);
 
 	if (TransactionIdPrecedes(MyProc->xmin, minSnapshot->xmin))
 		MyProc->xmin = TransactionXmin = minSnapshot->xmin;
@@ -1038,21 +940,7 @@ AtSubAbort_Snapshot(int level)
 
 		next = ActiveSnapshot->as_next;
 
-		/*
-		 * Decrement the snapshot's active count.  If it's still registered or
-		 * marked as active by an outer subtransaction, we can't free it yet.
-		 */
-		Assert(ActiveSnapshot->as_snap->active_count >= 1);
-		ActiveSnapshot->as_snap->active_count -= 1;
-
-		if (ActiveSnapshot->as_snap->active_count == 0 &&
-			ActiveSnapshot->as_snap->regd_count == 0)
-		{
-			dlist_delete(&ActiveSnapshot->as_snap->node);
-			FreeMVCCSnapshot(ActiveSnapshot->as_snap);
-		}
-
-		/* and free the stack element */
+		ReleaseMVCCSnapshotShared(ActiveSnapshot->as_snap.shared);
 		pfree(ActiveSnapshot);
 
 		ActiveSnapshot = next;
@@ -1068,6 +956,8 @@ AtSubAbort_Snapshot(int level)
 void
 AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 {
+	dlist_mutable_iter iter;
+
 	/*
 	 * If we exported any snapshots, clean them up.
 	 */
@@ -1094,7 +984,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 				elog(WARNING, "could not unlink file \"%s\": %m",
 					 esnap->snapfile);
 
-			dlist_delete(&esnap->snapshot->node);
+			ReleaseMVCCSnapshotShared(esnap->snapshot);
 		}
 
 		exportedSnapshots = NIL;
@@ -1103,17 +993,20 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	/* Drop all static snapshot */
 	if (CatalogSnapshotData.valid)
 	{
-		dlist_delete(&CatalogSnapshotData.node);
+		ReleaseMVCCSnapshotShared(CatalogSnapshotData.shared);
+		CatalogSnapshotData.shared = NULL;
 		CatalogSnapshotData.valid = false;
 	}
 	if (CurrentSnapshotData.valid)
 	{
-		dlist_delete(&CurrentSnapshotData.node);
+		ReleaseMVCCSnapshotShared(CurrentSnapshotData.shared);
+		CurrentSnapshotData.shared = NULL;
 		CurrentSnapshotData.valid = false;
 	}
 	if (SecondarySnapshotData.valid)
 	{
-		dlist_delete(&SecondarySnapshotData.node);
+		ReleaseMVCCSnapshotShared(SecondarySnapshotData.shared);
+		SecondarySnapshotData.shared = NULL;
 		SecondarySnapshotData.valid = false;
 	}
 
@@ -1134,11 +1027,23 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * And reset our state.  We don't need to free the memory explicitly --
 	 * it'll go away with TopTransactionContext.
 	 */
-	ActiveSnapshot = NULL;
-	dlist_init(&ValidSnapshots);
+	dlist_foreach_modify(iter, &ValidSnapshots)
+	{
+		MVCCSnapshotShared cur = dlist_container(MVCCSnapshotSharedData, node, iter.cur);
 
-	CurrentSnapshotData.valid = false;
-	SecondarySnapshotData.valid = false;
+		dlist_delete(iter.cur);
+		cur->refcount = 0;
+		if (cur == latestSnapshotShared)
+		{
+			/* keep it */
+		}
+		else if (spareSnapshotShared == NULL)
+			spareSnapshotShared = cur;
+		else
+			pfree(cur);
+	}
+
+	ActiveSnapshot = NULL;
 	FirstSnapshotSet = false;
 	FirstXactSnapshotRegistered = false;
 
@@ -1161,9 +1066,8 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
  *		snapshot.
  */
 char *
-ExportSnapshot(MVCCSnapshot snapshot)
+ExportSnapshot(MVCCSnapshotShared snapshot)
 {
-	MVCCSnapshot orig_snapshot;
 	TransactionId topXid;
 	TransactionId *children;
 	ExportedSnapshot *esnap;
@@ -1224,23 +1128,23 @@ ExportSnapshot(MVCCSnapshot snapshot)
 	 * Copy the snapshot into TopTransactionContext, add it to the
 	 * exportedSnapshots list, and mark it pseudo-registered.  We do this to
 	 * ensure that the snapshot's xmin is honored for the rest of the
-	 * transaction.
+	 * transaction. XXX
 	 */
-	orig_snapshot = snapshot;
-	snapshot = CopyMVCCSnapshot(orig_snapshot);
-
 	oldcxt = MemoryContextSwitchTo(TopTransactionContext);
 	esnap = palloc_object(ExportedSnapshot);
 	esnap->snapfile = pstrdup(path);
 	esnap->snapshot = snapshot;
+	snapshot->refcount++;
 	exportedSnapshots = lappend(exportedSnapshots, esnap);
 	MemoryContextSwitchTo(oldcxt);
 
+#if 0 // fixme
 	snapshot->regd_count++;
 	if (orig_snapshot->regd_count > 0)
 		dlist_insert_after(&orig_snapshot->node, &snapshot->node);
 	else
 		valid_snapshots_push_tail(snapshot);
+#endif
 
 	/*
 	 * Fill buf with a text serialization of the snapshot, plus identification
@@ -1261,8 +1165,8 @@ ExportSnapshot(MVCCSnapshot snapshot)
 	/*
 	 * We must include our own top transaction ID in the top-xid data, since
 	 * by definition we will still be running when the importing transaction
-	 * adopts the snapshot, but GetSnapshotData never includes our own XID in
-	 * the snapshot.  (There must, therefore, be enough room to add it.)
+	 * adopts the snapshot, but GetMVCCSnapshotData never includes our own XID
+	 * in the snapshot.  (There must, therefore, be enough room to add it.)
 	 *
 	 * However, it could be that our topXid is after the xmax, in which case
 	 * we shouldn't include it because xip[] members are expected to be before
@@ -1347,7 +1251,7 @@ pg_export_snapshot(PG_FUNCTION_ARGS)
 {
 	char	   *snapshotName;
 
-	snapshotName = ExportSnapshot((MVCCSnapshot) GetActiveSnapshot());
+	snapshotName = ExportSnapshot(((MVCCSnapshot) GetActiveSnapshot())->shared);
 	PG_RETURN_TEXT_P(cstring_to_text(snapshotName));
 }
 
@@ -1451,7 +1355,7 @@ ImportSnapshot(const char *idstr)
 	Oid			src_dbid;
 	int			src_isolevel;
 	bool		src_readonly;
-	MVCCSnapshotData snapshot;
+	MVCCSnapshotShared snapshot;
 
 	/*
 	 * Must be at top level of a fresh transaction.  Note in particular that
@@ -1521,8 +1425,6 @@ ImportSnapshot(const char *idstr)
 	/*
 	 * Construct a snapshot struct by parsing the file content.
 	 */
-	memset(&snapshot, 0, sizeof(snapshot));
-
 	parseVxidFromText("vxid:", &filebuf, path, &src_vxid);
 	src_pid = parseIntFromText("pid:", &filebuf, path);
 	/* we abuse parseXidFromText a bit here ... */
@@ -1530,12 +1432,11 @@ ImportSnapshot(const char *idstr)
 	src_isolevel = parseIntFromText("iso:", &filebuf, path);
 	src_readonly = parseIntFromText("ro:", &filebuf, path);
 
-	snapshot.snapshot_type = SNAPSHOT_MVCC;
+	snapshot = AllocMVCCSnapshotShared();
+	snapshot->xmin = parseXidFromText("xmin:", &filebuf, path);
+	snapshot->xmax = parseXidFromText("xmax:", &filebuf, path);
 
-	snapshot.xmin = parseXidFromText("xmin:", &filebuf, path);
-	snapshot.xmax = parseXidFromText("xmax:", &filebuf, path);
-
-	snapshot.xcnt = xcnt = parseIntFromText("xcnt:", &filebuf, path);
+	snapshot->xcnt = xcnt = parseIntFromText("xcnt:", &filebuf, path);
 
 	/* sanity-check the xid count before palloc */
 	if (xcnt < 0 || xcnt > GetMaxSnapshotXidCount())
@@ -1543,15 +1444,15 @@ ImportSnapshot(const char *idstr)
 				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				 errmsg("invalid snapshot data in file \"%s\"", path)));
 
-	snapshot.xip = (TransactionId *) palloc(xcnt * sizeof(TransactionId));
+	snapshot->xip = (TransactionId *) palloc(xcnt * sizeof(TransactionId));
 	for (i = 0; i < xcnt; i++)
-		snapshot.xip[i] = parseXidFromText("xip:", &filebuf, path);
+		snapshot->xip[i] = parseXidFromText("xip:", &filebuf, path);
 
-	snapshot.suboverflowed = parseIntFromText("sof:", &filebuf, path);
+	snapshot->suboverflowed = parseIntFromText("sof:", &filebuf, path);
 
-	if (!snapshot.suboverflowed)
+	if (!snapshot->suboverflowed)
 	{
-		snapshot.subxcnt = xcnt = parseIntFromText("sxcnt:", &filebuf, path);
+		snapshot->subxcnt = xcnt = parseIntFromText("sxcnt:", &filebuf, path);
 
 		/* sanity-check the xid count before palloc */
 		if (xcnt < 0 || xcnt > GetMaxSnapshotSubxidCount())
@@ -1559,17 +1460,19 @@ ImportSnapshot(const char *idstr)
 					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					 errmsg("invalid snapshot data in file \"%s\"", path)));
 
-		snapshot.subxip = (TransactionId *) palloc(xcnt * sizeof(TransactionId));
+		snapshot->subxip = (TransactionId *) palloc(xcnt * sizeof(TransactionId));
 		for (i = 0; i < xcnt; i++)
-			snapshot.subxip[i] = parseXidFromText("sxp:", &filebuf, path);
+			snapshot->subxip[i] = parseXidFromText("sxp:", &filebuf, path);
 	}
 	else
 	{
-		snapshot.subxcnt = 0;
-		snapshot.subxip = NULL;
+		snapshot->subxcnt = 0;
 	}
 
-	snapshot.takenDuringRecovery = parseIntFromText("rec:", &filebuf, path);
+	snapshot->takenDuringRecovery = parseIntFromText("rec:", &filebuf, path);
+
+	snapshot->refcount = 1;
+	valid_snapshots_push_out_of_order(snapshot);
 
 	/*
 	 * Do some additional sanity checking, just to protect ourselves.  We
@@ -1578,8 +1481,8 @@ ImportSnapshot(const char *idstr)
 	 */
 	if (!VirtualTransactionIdIsValid(src_vxid) ||
 		!OidIsValid(src_dbid) ||
-		!TransactionIdIsNormal(snapshot.xmin) ||
-		!TransactionIdIsNormal(snapshot.xmax))
+		!TransactionIdIsNormal(snapshot->xmin) ||
+		!TransactionIdIsNormal(snapshot->xmax))
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				 errmsg("invalid snapshot data in file \"%s\"", path)));
@@ -1617,7 +1520,7 @@ ImportSnapshot(const char *idstr)
 				 errmsg("cannot import a snapshot from a different database")));
 
 	/* OK, install the snapshot */
-	SetTransactionSnapshot(&snapshot, &src_vxid, src_pid, NULL);
+	SetTransactionSnapshot(snapshot, &src_vxid, src_pid, NULL);
 }
 
 /*
@@ -1683,18 +1586,21 @@ ThereAreNoPriorRegisteredSnapshots(void)
 
 	dlist_foreach(iter, &ValidSnapshots)
 	{
-		MVCCSnapshot cur = dlist_container(MVCCSnapshotData, node, iter.cur);
+		MVCCSnapshotShared cur =
+			dlist_container(MVCCSnapshotSharedData, node, iter.cur);
+		uint32		allowedcount = 0;
 
 		if (FirstXactSnapshotRegistered)
 		{
 			Assert(CurrentSnapshotData.valid);
-			if (cur != &CurrentSnapshotData)
-				continue;
+			if (cur == CurrentSnapshotData.shared)
+				allowedcount++;
 		}
-		if (ActiveSnapshot && cur == ActiveSnapshot->as_snap)
-			continue;
+		if (ActiveSnapshot && cur == ActiveSnapshot->as_snap.shared)
+			allowedcount++;
 
-		return false;
+		if (cur->refcount != allowedcount)
+			return false;
 	}
 
 	return true;
@@ -1720,8 +1626,9 @@ HaveRegisteredOrActiveSnapshot(void)
 	 * registered more than one snapshot has to be in ValidSnapshots.
 	 */
 	if (CatalogSnapshotData.valid &&
-		dlist_head_node(&ValidSnapshots) == &CatalogSnapshotData.node &&
-		dlist_tail_node(&ValidSnapshots) == &CatalogSnapshotData.node)
+		CatalogSnapshotData.shared->refcount == 1 &&
+		dlist_head_node(&ValidSnapshots) == &CatalogSnapshotData.shared->node &&
+		dlist_tail_node(&ValidSnapshots) == &CatalogSnapshotData.shared->node)
 	{
 		return false;
 	}
@@ -1788,11 +1695,11 @@ EstimateSnapshotSpace(MVCCSnapshot snapshot)
 
 	/* We allocate any XID arrays needed in the same palloc block. */
 	size = add_size(sizeof(SerializedSnapshotData),
-					mul_size(snapshot->xcnt, sizeof(TransactionId)));
-	if (snapshot->subxcnt > 0 &&
-		(!snapshot->suboverflowed || snapshot->takenDuringRecovery))
+					mul_size(snapshot->shared->xcnt, sizeof(TransactionId)));
+	if (snapshot->shared->subxcnt > 0 &&
+		(!snapshot->shared->suboverflowed || snapshot->shared->takenDuringRecovery))
 		size = add_size(size,
-						mul_size(snapshot->subxcnt, sizeof(TransactionId)));
+						mul_size(snapshot->shared->subxcnt, sizeof(TransactionId)));
 
 	return size;
 }
@@ -1807,15 +1714,15 @@ SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 
-	Assert(snapshot->subxcnt >= 0);
+	Assert(snapshot->shared->subxcnt >= 0);
 
 	/* Copy all required fields */
-	serialized_snapshot.xmin = snapshot->xmin;
-	serialized_snapshot.xmax = snapshot->xmax;
-	serialized_snapshot.xcnt = snapshot->xcnt;
-	serialized_snapshot.subxcnt = snapshot->subxcnt;
-	serialized_snapshot.suboverflowed = snapshot->suboverflowed;
-	serialized_snapshot.takenDuringRecovery = snapshot->takenDuringRecovery;
+	serialized_snapshot.xmin = snapshot->shared->xmin;
+	serialized_snapshot.xmax = snapshot->shared->xmax;
+	serialized_snapshot.xcnt = snapshot->shared->xcnt;
+	serialized_snapshot.subxcnt = snapshot->shared->subxcnt;
+	serialized_snapshot.suboverflowed = snapshot->shared->suboverflowed;
+	serialized_snapshot.takenDuringRecovery = snapshot->shared->takenDuringRecovery;
 	serialized_snapshot.curcid = snapshot->curcid;
 
 	/*
@@ -1823,7 +1730,7 @@ SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 	 * taken during recovery - in that case, top-level XIDs are in subxip as
 	 * well, and we mustn't lose them.
 	 */
-	if (serialized_snapshot.suboverflowed && !snapshot->takenDuringRecovery)
+	if (serialized_snapshot.suboverflowed && !snapshot->shared->takenDuringRecovery)
 		serialized_snapshot.subxcnt = 0;
 
 	/* Copy struct to possibly-unaligned buffer */
@@ -1831,10 +1738,10 @@ SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 		   &serialized_snapshot, sizeof(SerializedSnapshotData));
 
 	/* Copy XID array */
-	if (snapshot->xcnt > 0)
+	if (snapshot->shared->xcnt > 0)
 		memcpy((TransactionId *) (start_address +
 								  sizeof(SerializedSnapshotData)),
-			   snapshot->xip, snapshot->xcnt * sizeof(TransactionId));
+			   snapshot->shared->xip, snapshot->shared->xcnt * sizeof(TransactionId));
 
 	/*
 	 * Copy SubXID array. Don't bother to copy it if it had overflowed,
@@ -1845,10 +1752,10 @@ SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 	if (serialized_snapshot.subxcnt > 0)
 	{
 		Size		subxipoff = sizeof(SerializedSnapshotData) +
-			snapshot->xcnt * sizeof(TransactionId);
+			snapshot->shared->xcnt * sizeof(TransactionId);
 
 		memcpy((TransactionId *) (start_address + subxipoff),
-			   snapshot->subxip, snapshot->subxcnt * sizeof(TransactionId));
+			   snapshot->shared->subxip, snapshot->shared->subxcnt * sizeof(TransactionId));
 	}
 }
 
@@ -1876,49 +1783,46 @@ RestoreSnapshot(char *start_address)
 	size = sizeof(MVCCSnapshotData)
 		+ serialized_snapshot.xcnt * sizeof(TransactionId)
 		+ serialized_snapshot.subxcnt * sizeof(TransactionId);
+	Assert(serialized_snapshot.xcnt <= GetMaxSnapshotXidCount());
+	Assert(serialized_snapshot.subxcnt <= GetMaxSnapshotSubxidCount());
 
 	/* Copy all required fields */
 	snapshot = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
 	snapshot->snapshot_type = SNAPSHOT_MVCC;
-	snapshot->xmin = serialized_snapshot.xmin;
-	snapshot->xmax = serialized_snapshot.xmax;
-	snapshot->xip = NULL;
-	snapshot->xcnt = serialized_snapshot.xcnt;
-	snapshot->subxip = NULL;
-	snapshot->subxcnt = serialized_snapshot.subxcnt;
-	snapshot->suboverflowed = serialized_snapshot.suboverflowed;
-	snapshot->takenDuringRecovery = serialized_snapshot.takenDuringRecovery;
+	snapshot->kind = SNAPSHOT_REGISTERED;
+	snapshot->shared = AllocMVCCSnapshotShared();
+	snapshot->shared->xmin = serialized_snapshot.xmin;
+	snapshot->shared->xmax = serialized_snapshot.xmax;
+	snapshot->shared->xcnt = serialized_snapshot.xcnt;
+	snapshot->shared->subxcnt = serialized_snapshot.subxcnt;
+	snapshot->shared->suboverflowed = serialized_snapshot.suboverflowed;
+	snapshot->shared->takenDuringRecovery = serialized_snapshot.takenDuringRecovery;
+	snapshot->shared->snapXactCompletionCount = 0;
+
+	snapshot->shared->refcount = 1;
+	valid_snapshots_push_out_of_order(snapshot->shared);
+
 	snapshot->curcid = serialized_snapshot.curcid;
-	snapshot->snapXactCompletionCount = 0;
 
 	/* Copy XIDs, if present. */
 	if (serialized_snapshot.xcnt > 0)
 	{
-		snapshot->xip = (TransactionId *) (snapshot + 1);
-		memcpy(snapshot->xip, serialized_xids,
+		memcpy(snapshot->shared->xip, serialized_xids,
 			   serialized_snapshot.xcnt * sizeof(TransactionId));
 	}
 
 	/* Copy SubXIDs, if present. */
 	if (serialized_snapshot.subxcnt > 0)
 	{
-		snapshot->subxip = ((TransactionId *) (snapshot + 1)) +
-			serialized_snapshot.xcnt;
-		memcpy(snapshot->subxip, serialized_xids + serialized_snapshot.xcnt,
+		memcpy(snapshot->shared->subxip, serialized_xids + serialized_snapshot.xcnt,
 			   serialized_snapshot.subxcnt * sizeof(TransactionId));
 	}
 
-	/* Set the copied flag so that the caller will set refcounts correctly. */
-	snapshot->regd_count = 0;
-	snapshot->active_count = 0;
-	snapshot->copied = true;
 	snapshot->valid = true;
 
 	/* and tell resowner.c about it, just like RegisterSnapshot() */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
-	snapshot->regd_count++;
 	ResourceOwnerRememberSnapshot(CurrentResourceOwner, (Snapshot) snapshot);
-	valid_snapshots_push_out_of_order(snapshot);
 
 	return snapshot;
 }
@@ -1929,21 +1833,21 @@ RestoreSnapshot(char *start_address)
 void
 RestoreTransactionSnapshot(MVCCSnapshot snapshot, PGPROC *source_pgproc)
 {
-	SetTransactionSnapshot(snapshot, NULL, InvalidPid, source_pgproc);
+	SetTransactionSnapshot(snapshot->shared, NULL, InvalidPid, source_pgproc);
 }
 
 /*
  * XidInMVCCSnapshot
  *		Is the given XID still-in-progress according to the snapshot?
  *
- * Note: GetSnapshotData never stores either top xid or subxids of our own
- * backend into a snapshot, so these xids will not be reported as "running"
- * by this function.  This is OK for current uses, because we always check
- * TransactionIdIsCurrentTransactionId first, except when it's known the
- * XID could not be ours anyway.
+ * Note: GetMVCCSnapshotData never stores either top xid or subxids of our own
+ * backend into a snapshot, so these xids will not be reported as "running" by
+ * this function.  This is OK for current uses, because we always check
+ * TransactionIdIsCurrentTransactionId first, except when it's known the XID
+ * could not be ours anyway.
  */
 bool
-XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot)
+XidInMVCCSnapshot(TransactionId xid, MVCCSnapshotShared snapshot)
 {
 	/*
 	 * Make a quick range check to eliminate most XIDs without looking at the
@@ -2039,6 +1943,84 @@ XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot)
 	return false;
 }
 
+/*
+ * Allocate an MVCCSnapshotShared struct
+ *
+ * The 'xip' and 'subxip' arrays are allocated so that they can hold the max
+ * number of XIDs. That's usually overkill, but it allows us to do the
+ * allocation while not holding ProcArrayLock.
+ *
+ * MVCCSnapshotShared structs are kept in TopMemoryContext and refcounted.
+ * The refcount is initially zero, the caller is expected to increment it.
+ */
+MVCCSnapshotShared
+AllocMVCCSnapshotShared(void)
+{
+	MemoryContext save_cxt;
+	MVCCSnapshotShared shared;
+	size_t		size;
+	char	   *p;
+
+	/*
+	 * To reduce alloc/free overhead in GetMVCCSnapshotData(), we have a
+	 * single-element pool.
+	 */
+	if (spareSnapshotShared)
+	{
+		shared = spareSnapshotShared;
+		spareSnapshotShared = NULL;
+		return shared;
+	}
+
+	save_cxt = MemoryContextSwitchTo(TopMemoryContext);
+
+	size = sizeof(MVCCSnapshotSharedData) +
+		GetMaxSnapshotXidCount() * sizeof(TransactionId) +
+		GetMaxSnapshotSubxidCount() * sizeof(TransactionId);
+	p = palloc(size);
+
+	shared = (MVCCSnapshotShared) p;
+	p += sizeof(MVCCSnapshotSharedData);
+	shared->xip = (TransactionId *) p;
+	p += GetMaxSnapshotXidCount() * sizeof(TransactionId);
+	shared->subxip = (TransactionId *) p;
+
+	shared->snapXactCompletionCount = 0;
+	shared->refcount = 0;
+
+	MemoryContextSwitchTo(save_cxt);
+
+	return shared;
+}
+
+/*
+ * Decrement the refcount on an MVCCSnapshotShared struct, freeing it if it
+ * reaches zero.
+ */
+static void
+ReleaseMVCCSnapshotShared(MVCCSnapshotShared shared)
+{
+	Assert(shared->refcount > 0);
+	shared->refcount--;
+
+	if (shared->refcount == 0)
+	{
+		dlist_delete(&shared->node);
+		if (shared != latestSnapshotShared)
+			FreeMVCCSnapshotShared(shared);
+	}
+}
+
+void
+FreeMVCCSnapshotShared(MVCCSnapshotShared shared)
+{
+	Assert(shared->refcount == 0);
+	if (spareSnapshotShared == NULL)
+		spareSnapshotShared = shared;
+	else
+		pfree(shared);
+}
+
 /* ResourceOwner callbacks */
 
 static void
@@ -2052,12 +2034,13 @@ ResOwnerReleaseSnapshot(Datum res)
 
 /* dlist_push_tail, with assertion that the list stays ordered by xmin */
 static void
-valid_snapshots_push_tail(MVCCSnapshot snapshot)
+valid_snapshots_push_tail(MVCCSnapshotShared snapshot)
 {
 #ifdef USE_ASSERT_CHECKING
 	if (!dlist_is_empty(&ValidSnapshots))
 	{
-		MVCCSnapshot tail = dlist_tail_element(MVCCSnapshotData, node, &ValidSnapshots);
+		MVCCSnapshotShared tail =
+			dlist_tail_element(MVCCSnapshotSharedData, node, &ValidSnapshots);
 
 		Assert(TransactionIdFollowsOrEquals(snapshot->xmin, tail->xmin));
 	}
@@ -2072,13 +2055,14 @@ valid_snapshots_push_tail(MVCCSnapshot snapshot)
  * the list is small.
  */
 static void
-valid_snapshots_push_out_of_order(MVCCSnapshot snapshot)
+valid_snapshots_push_out_of_order(MVCCSnapshotShared snapshot)
 {
 	dlist_iter	iter;
 
 	dlist_foreach(iter, &ValidSnapshots)
 	{
-		MVCCSnapshot cur = dlist_container(MVCCSnapshotData, node, iter.cur);
+		MVCCSnapshotShared cur =
+			dlist_container(MVCCSnapshotSharedData, node, iter.cur);
 
 		if (TransactionIdFollowsOrEquals(snapshot->xmin, cur->xmin))
 		{
diff --git a/src/include/access/transam.h b/src/include/access/transam.h
index c9e20418275..97adaae8f28 100644
--- a/src/include/access/transam.h
+++ b/src/include/access/transam.h
@@ -242,8 +242,8 @@ typedef struct TransamVariablesData
 	 * Number of top-level transactions with xids (i.e. which may have
 	 * modified the database) that completed in some form since the start of
 	 * the server. This currently is solely used to check whether
-	 * GetSnapshotData() needs to recompute the contents of the snapshot, or
-	 * not. There are likely other users of this.  Always above 1.
+	 * GetMVCCSnapshotData() needs to recompute the contents of the snapshot,
+	 * or not. There are likely other users of this.  Always above 1.
 	 */
 	uint64		xactCompletionCount;
 
diff --git a/src/include/storage/predicate.h b/src/include/storage/predicate.h
index 9a7f53c900c..b1f48b7d1be 100644
--- a/src/include/storage/predicate.h
+++ b/src/include/storage/predicate.h
@@ -47,10 +47,10 @@ extern void CheckPointPredicate(void);
 extern bool PageIsPredicateLocked(Relation relation, BlockNumber blkno);
 
 /* predicate lock maintenance */
-extern MVCCSnapshot GetSerializableTransactionSnapshot(MVCCSnapshot snapshot);
-extern void SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
-											   VirtualTransactionId *sourcevxid,
-											   int sourcepid);
+extern MVCCSnapshotShared GetSerializableTransactionSnapshotData(void);
+extern void SetSerializableTransactionSnapshotData(MVCCSnapshotShared snapshot,
+												   VirtualTransactionId *sourcevxid,
+												   int sourcepid);
 extern void RegisterPredicateLockingXid(TransactionId xid);
 extern void PredicateLockRelation(Relation relation, Snapshot snapshot);
 extern void PredicateLockPage(Relation relation, BlockNumber blkno, Snapshot snapshot);
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index c6f5ebceefd..26db4098a57 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -340,7 +340,7 @@ extern PGDLLIMPORT PGPROC *MyProc;
  * Adding/Removing an entry into the procarray requires holding *both*
  * ProcArrayLock and XidGenLock in exclusive mode (in that order). Both are
  * needed because the dense arrays (see below) are accessed from
- * GetNewTransactionId() and GetSnapshotData(), and we don't want to add
+ * GetNewTransactionId() and GetMVCCSnapshotData(), and we don't want to add
  * further contention by both using the same lock. Adding/Removing a procarray
  * entry is much less frequent.
  *
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index c40c2ee5d1b..b5700eb9d86 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -44,7 +44,7 @@ extern void KnownAssignedTransactionIdsIdleMaintenance(void);
 extern int	GetMaxSnapshotXidCount(void);
 extern int	GetMaxSnapshotSubxidCount(void);
 
-extern MVCCSnapshot GetSnapshotData(MVCCSnapshot snapshot);
+extern MVCCSnapshotShared GetMVCCSnapshotData(void);
 
 extern bool ProcArrayInstallImportedXmin(TransactionId xmin,
 										 VirtualTransactionId *sourcevxid);
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index 5d85526b4c9..519cb3b43bd 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -59,6 +59,13 @@ extern PGDLLIMPORT SnapshotData SnapshotToastData;
 #define IsHistoricMVCCSnapshot(snapshot)  \
 	((snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
 
+/* exported so that GetMVCCSnapshotData() can access these */
+extern MVCCSnapshotShared latestSnapshotShared;
+extern MVCCSnapshotShared spareSnapshotShared;
+
+extern MVCCSnapshotShared AllocMVCCSnapshotShared(void);
+extern void FreeMVCCSnapshotShared(MVCCSnapshotShared shared);
+
 extern Snapshot GetTransactionSnapshot(void);
 extern Snapshot GetLatestSnapshot(void);
 extern void SnapshotSetCommandId(CommandId curcid);
@@ -92,7 +99,7 @@ extern void WaitForOlderSnapshots(TransactionId limitXmin, bool progress);
 extern bool ThereAreNoPriorRegisteredSnapshots(void);
 extern bool HaveRegisteredOrActiveSnapshot(void);
 
-extern char *ExportSnapshot(MVCCSnapshot snapshot);
+extern char *ExportSnapshot(MVCCSnapshotShared snapshot);
 
 /*
  * These live in procarray.c because they're intimately linked to the
@@ -108,7 +115,7 @@ extern bool GlobalVisCheckRemovableFullXid(Relation rel, FullTransactionId fxid)
 /*
  * Utility functions for implementing visibility routines in table AMs.
  */
-extern bool XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot);
+extern bool XidInMVCCSnapshot(TransactionId xid, MVCCSnapshotShared snapshot);
 
 /* Support for catalog timetravel for logical decoding */
 struct HTAB;
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 44b3b20f73c..193366ce052 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -119,17 +119,44 @@ typedef enum SnapshotType
 	SNAPSHOT_NON_VACUUMABLE,
 } SnapshotType;
 
+typedef struct MVCCSnapshotSharedData *MVCCSnapshotShared;
+
+typedef enum MVCCSnapshotKind
+{
+	SNAPSHOT_STATIC,
+	SNAPSHOT_ACTIVE,
+	SNAPSHOT_REGISTERED,
+} MVCCSnapshotKind;
+
 /*
  * Struct representing a normal MVCC snapshot.
  *
  * MVCC snapshots come in two variants: those taken during recovery in hot
  * standby mode, and "normal" MVCC snapshots.  They are distinguished by
- * takenDuringRecovery.
+ * shared->takenDuringRecovery.
  */
 typedef struct MVCCSnapshotData
 {
 	SnapshotType snapshot_type; /* type of snapshot, must be first */
 
+	/*
+	 * Most fields are in this separate struct which can be reused and shared
+	 * between snapshots that only differ in the command ID.  It is reference
+	 * counted separately.
+	 */
+	MVCCSnapshotShared shared;
+
+	CommandId	curcid;			/* in my xact, CID < curcid are visible */
+
+	/*
+	 * Book-keeping information, used by the snapshot manager
+	 */
+	MVCCSnapshotKind kind;
+	bool		valid;
+} MVCCSnapshotData;
+
+typedef struct MVCCSnapshotSharedData
+{
 	/*
 	 * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see
 	 * the effects of all older XIDs except those listed in the snapshot. xmin
@@ -160,25 +187,17 @@ typedef struct MVCCSnapshotData
 	bool		suboverflowed;	/* has the subxip array overflowed? */
 
 	bool		takenDuringRecovery;	/* recovery-shaped snapshot? */
-	bool		copied;			/* false if it's a static snapshot */
-	bool		valid;			/* is this snapshot valid? */
-
-	CommandId	curcid;			/* in my xact, CID < curcid are visible */
-
-	/*
-	 * Book-keeping information, used by the snapshot manager
-	 */
-	uint32		active_count;	/* refcount on ActiveSnapshot stack */
-	uint32		regd_count;		/* refcount of registrations in resowners */
-	dlist_node	node;			/* link in ValidSnapshots */
 
 	/*
-	 * The transaction completion count at the time GetSnapshotData() built
-	 * this snapshot. Allows to avoid re-computing static snapshots when no
-	 * transactions completed since the last GetSnapshotData().
+	 * The transaction completion count at the time GetMVCCSnapshotData()
+	 * built this snapshot. Allows to avoid re-computing static snapshots when
+	 * no transactions completed since the last GetMVCCSnapshotData().
 	 */
 	uint64		snapXactCompletionCount;
-} MVCCSnapshotData;
+
+	uint32		refcount;
+	dlist_node	node;			/* link in ValidSnapshots */
+} MVCCSnapshotSharedData;
 
 typedef struct MVCCSnapshotData *MVCCSnapshot;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8f26256464f..80f0f1499b2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1676,6 +1676,8 @@ MINIDUMP_TYPE
 MJEvalResult
 MTTargetRelLookup
 MVCCSnapshotData
+MVCCSnapshotKind
+MVCCSnapshotSharedData
 MVDependencies
 MVDependency
 MVNDistinct
-- 
2.47.3



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
@ 2025-12-19 05:15 ` Chao Li <[email protected]>
  2025-12-19 11:40   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 11:51   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  1 sibling, 2 replies; 26+ messages in thread

From: Chao Li @ 2025-12-19 05:15 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: pgsql-hackers



> On Dec 19, 2025, at 07:30, Heikki Linnakangas <[email protected]> wrote:
> 
> This is a continuation of the earlier thread with the same subject [1], and related to the CSN work [2].
> 
> I'm pretty happy patches 0001-0005. They make the snapshot management more clear in many ways:
> 
> Patch 0001: Use a proper type for RestoreTransactionSnapshot's PGPROC arg
> 
> Minor cleanup, independent of the rest of the patches
> 

Looks like this cleanup should have done earlier. The old comments says that only reason to use void * is to avoid include a header file. But in snapmgr.h, there already has a usage of “struct HTAB” from 12 years ago.

Maybe we can also do:
```
typedef struct PGPROC PGPROC;
extern void RestoreTransactionSnapshot(MVCCSnapshot snapshot, PGPROC *source_pgproc);
```

So the function declaration looks neater. snapmgr.h line 101 has done in this way. If not pretty much burden, we can update HTAB as well.

I believe 0001 is an independent self-contained commit that can be pushed first.

> 
> Patch 0002: Split SnapshotData into separate structs for each kind of snapshot
> 
> This implements the long-standing TODO and splits SnapshotData up into multiple structs. This makes it more clear which fields are used with which kind of snapshot. For example, we now have properly named fields for the XID arrays in historic snapshots. Previously, they abused the 'xip' and 'subxip' arrays to mean something different than what they mean in regular MVCC snapshots.
> 
> This introduces some new casts between Snapshots and the new MVCCSnapshots. I struggled to decide which functions should use the new MVCCSnapshot type and which should continue to use Snapshot. It's a balancing act between code churn and where we want to have casts. For example, PushActiveSnapshot() takes a Snapshot argument, but assumes that it's an MVCC snapshot, so it really should take MVCCSnapshot. But most of its callers have a Snapshot rather than MVCCSnapshot. Another example: the executor assumes that it's dealing with MVCC snapshots, so it would be appropriate to use MVCCSnapshot for EState->es_snapshot. But it'd cause a lot code churn and casts elsewhere. I think the patch is a reasonable middle ground.
> 

The core of 0002 is the new union Snapshot:
```
+/*
+ * Generic union representing all kind of possible snapshots.  Some have
+ * type-specific structs.
+ */
+typedef union SnapshotData
+{
+	SnapshotType snapshot_type; /* type of snapshot */
+
+	MVCCSnapshotData mvcc;
+	DirtySnapshotData dirty;
+	HistoricMVCCSnapshotData historic_mvcc;
+	NonVacuumableSnapshotData nonvacuumable;
 } SnapshotData;
```

And my big concern is here. This union definition looks unusual, snapshot_type shares the same space with real snapshot bodies, so that once snapshot is assigned to the union, that type info is lost, there would be no way to decide what exact snapshot is stored in SnapshotData.

I think SnapshotData should be a structure and utilize anonymous union technique:
```
typedef struct SnapshotData
{
    SnapshotType snapshot_type; /* type of snapshot */

    union {
       MVCCSnapshotData mvcc;
       DirtySnapshotData dirty;
       HistoricMVCCSnapshotData historic_mvcc;
       NonVacuumableSnapshotData nonvacuumable;
    }
```

If you agree with this, then 0002 will need some rework. A set of helper micros/functions would need to be added, e.g. InitDirtySnapshot(), DirtySnapshotGetSnapshot()...

So that, I’d stop reviewing, and see what do you think.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/









^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
@ 2025-12-19 11:40   ` Heikki Linnakangas <[email protected]>
  1 sibling, 0 replies; 26+ messages in thread

From: Heikki Linnakangas @ 2025-12-19 11:40 UTC (permalink / raw)
  To: Chao Li <[email protected]>; +Cc: pgsql-hackers

On 19/12/2025 07:15, Chao Li wrote:
>> On Dec 19, 2025, at 07:30, Heikki Linnakangas <[email protected]> wrote:
>> Patch 0001: Use a proper type for RestoreTransactionSnapshot's PGPROC arg
>>
>> Minor cleanup, independent of the rest of the patches
> 
> Looks like this cleanup should have done earlier. The old comments says that only reason to use void * is to avoid include a header file. But in snapmgr.h, there already has a usage of “struct HTAB” from 12 years ago.
> 
> Maybe we can also do:
> ```
> typedef struct PGPROC PGPROC;
> extern void RestoreTransactionSnapshot(MVCCSnapshot snapshot, PGPROC *source_pgproc);
> ```
> 
> So the function declaration looks neater. snapmgr.h line 101 has done in this way. If not pretty much burden, we can update HTAB as well.

Yeah, we're not very consistent about that. I followed the example of 
HTAB and went with "struct PGPROC" for now.

> I believe 0001 is an independent self-contained commit that can be pushed first.

Pushed 0001, thanks for the review!

- Heikki






^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
@ 2025-12-19 11:51   ` Heikki Linnakangas <[email protected]>
  2025-12-19 21:32     ` Re: A few patches to clarify snapshot management, part 2 Kirill Reshke <[email protected]>
  2025-12-22 02:59     ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
  1 sibling, 2 replies; 26+ messages in thread

From: Heikki Linnakangas @ 2025-12-19 11:51 UTC (permalink / raw)
  To: Chao Li <[email protected]>; +Cc: pgsql-hackers

On 19/12/2025 07:15, Chao Li wrote:
>> On Dec 19, 2025, at 07:30, Heikki Linnakangas <[email protected]> wrote:
>>
>> Patch 0002: Split SnapshotData into separate structs for each kind of snapshot
>> >> This implements the long-standing TODO and splits SnapshotData up
>> into multiple structs. This makes it more clear which fields are
>> used with which kind of snapshot. For example, we now have
>> properly named fields for the XID arrays in historic snapshots.
>> Previously, they abused the 'xip' and 'subxip' arrays to mean
>> something different than what they mean in regular MVCC snapshots.
>> 
>> This introduces some new casts between Snapshots and the new
>> MVCCSnapshots. I struggled to decide which functions should use
>> the new MVCCSnapshot type and which should continue to use
>> Snapshot. It's a balancing act between code churn and where we
>> want to have casts. For example, PushActiveSnapshot() takes a
>> Snapshot argument, but assumes that it's an MVCC snapshot, so it
>> really should take MVCCSnapshot. But most of its callers have a
>> Snapshot rather than MVCCSnapshot. Another example: the executor
>> assumes that it's dealing with MVCC snapshots, so it would be
>> appropriate to use MVCCSnapshot for EState->es_snapshot. But it'd
>> cause a lot code churn and casts elsewhere. I think the patch is a
>> reasonable middle ground.
> > The core of 0002 is the new union Snapshot:
> ```
> +/*
> + * Generic union representing all kind of possible snapshots.  Some have
> + * type-specific structs.
> + */
> +typedef union SnapshotData
> +{
> +	SnapshotType snapshot_type; /* type of snapshot */
> +
> +	MVCCSnapshotData mvcc;
> +	DirtySnapshotData dirty;
> +	HistoricMVCCSnapshotData historic_mvcc;
> +	NonVacuumableSnapshotData nonvacuumable;
>   } SnapshotData;
> ```
> > And my big concern is here. This union definition looks unusual,
> snapshot_type shares the same space with real snapshot bodies, so
> that once snapshot is assigned to the union, that type info is lost,
> there would be no way to decide what exact snapshot is stored in
> SnapshotData.

Each of the structs, MVCCSnapshotData, DirtySnapshotData, etc., contain 
'snapshot_type' as the first field, so it's always available.

> I think SnapshotData should be a structure and utilize anonymous union technique:
> ```
> typedef struct SnapshotData
> {
>      SnapshotType snapshot_type; /* type of snapshot */
> 
>      union {
>         MVCCSnapshotData mvcc;
>         DirtySnapshotData dirty;
>         HistoricMVCCSnapshotData historic_mvcc;
>         NonVacuumableSnapshotData nonvacuumable;
>      }
> ```

Hmm, yeah, maybe that would be more clear. But then you cannot cast an 
"MVCCSnapshotData *" to "SnapshotData *", or vice versa.

> If you agree with this, then 0002 will need some rework. A set of helper micros/functions would need to be added, e.g. InitDirtySnapshot(), DirtySnapshotGetSnapshot()...

Right, I feel those helper macros / functions would be excessive.

- Heikki






^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
  2025-12-19 11:51   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
@ 2025-12-19 21:32     ` Kirill Reshke <[email protected]>
  1 sibling, 0 replies; 26+ messages in thread

From: Kirill Reshke @ 2025-12-19 21:32 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Chao Li <[email protected]>; pgsql-hackers

On Fri, 19 Dec 2025 at 16:51, Heikki Linnakangas <[email protected]> wrote:
>
> On 19/12/2025 07:15, Chao Li wrote:
> >> On Dec 19, 2025, at 07:30, Heikki Linnakangas <[email protected]> wrote:
> >>
> >> Patch 0002: Split SnapshotData into separate structs for each kind of snapshot
> >> >> This implements the long-standing TODO and splits SnapshotData up
> >> into multiple structs. This makes it more clear which fields are
> >> used with which kind of snapshot. For example, we now have
> >> properly named fields for the XID arrays in historic snapshots.
> >> Previously, they abused the 'xip' and 'subxip' arrays to mean
> >> something different than what they mean in regular MVCC snapshots.
> >>
> >> This introduces some new casts between Snapshots and the new
> >> MVCCSnapshots. I struggled to decide which functions should use
> >> the new MVCCSnapshot type and which should continue to use
> >> Snapshot. It's a balancing act between code churn and where we
> >> want to have casts. For example, PushActiveSnapshot() takes a
> >> Snapshot argument, but assumes that it's an MVCC snapshot, so it
> >> really should take MVCCSnapshot. But most of its callers have a
> >> Snapshot rather than MVCCSnapshot. Another example: the executor
> >> assumes that it's dealing with MVCC snapshots, so it would be
> >> appropriate to use MVCCSnapshot for EState->es_snapshot. But it'd
> >> cause a lot code churn and casts elsewhere. I think the patch is a
> >> reasonable middle ground.
> > > The core of 0002 is the new union Snapshot:
> > ```
> > +/*
> > + * Generic union representing all kind of possible snapshots.  Some have
> > + * type-specific structs.
> > + */
> > +typedef union SnapshotData
> > +{
> > +     SnapshotType snapshot_type; /* type of snapshot */
> > +
> > +     MVCCSnapshotData mvcc;
> > +     DirtySnapshotData dirty;
> > +     HistoricMVCCSnapshotData historic_mvcc;
> > +     NonVacuumableSnapshotData nonvacuumable;
> >   } SnapshotData;
> > ```
> > > And my big concern is here. This union definition looks unusual,
> > snapshot_type shares the same space with real snapshot bodies, so
> > that once snapshot is assigned to the union, that type info is lost,
> > there would be no way to decide what exact snapshot is stored in
> > SnapshotData.
>
> Each of the structs, MVCCSnapshotData, DirtySnapshotData, etc., contain
> 'snapshot_type' as the first field, so it's always available.
>
> > I think SnapshotData should be a structure and utilize anonymous union technique:
> > ```
> > typedef struct SnapshotData
> > {
> >      SnapshotType snapshot_type; /* type of snapshot */
> >
> >      union {
> >         MVCCSnapshotData mvcc;
> >         DirtySnapshotData dirty;
> >         HistoricMVCCSnapshotData historic_mvcc;
> >         NonVacuumableSnapshotData nonvacuumable;
> >      }
> > ```
>
> Hmm, yeah, maybe that would be more clear. But then you cannot cast an
> "MVCCSnapshotData *" to "SnapshotData *", or vice versa.
>
> > If you agree with this, then 0002 will need some rework. A set of helper micros/functions would need to be added, e.g. InitDirtySnapshot(), DirtySnapshotGetSnapshot()...
>
> Right, I feel those helper macros / functions would be excessive.
>
> - Heikki
>
>
>


Hi! I have been looking through this series of patches, when I noticed
that UnregisterSnapshotFromOwner/UnregisterSnapshot use this coding:

```
if (snapshot == InvalidSnapshot)
return;
```

First of all, I am not sure this is the type of check we usually do in
core. If this coding practice is OK, should we remove this check from
UnregisterSnapshot* caller?
Like in PFA.

Sorry for bikeshedding

-- 
Best regards,
Kirill Reshke


Attachments:

  [application/octet-stream] v1-0001-Unity-NULL-snapshot-hadling.patch (5.5K, ../../CALdSSPiHjqqqoOWECn5cgz98UXVOzbaDRsRuZerQcHc5OFDC6g@mail.gmail.com/2-v1-0001-Unity-NULL-snapshot-hadling.patch)
  download | inline diff:
From 358dda1591ca96bf8c6b9dbdc463148461d2709b Mon Sep 17 00:00:00 2001
From: reshke <[email protected]>
Date: Fri, 19 Dec 2025 21:23:38 +0000
Subject: [PATCH v1] Unity NULL snapshot hadling.

UnregisterSnapshot and UnregisterSnapshotFromOwner are ready to
accept NULL snapshot. However, there are places in code where we
call this function only for non-NULL argumnet. Remove this
unneccessary check to simplify coding.
---
 contrib/amcheck/verify_nbtree.c    | 7 ++++---
 src/backend/access/index/genam.c   | 9 +++++----
 src/backend/access/index/indexam.c | 4 ++--
 src/backend/libpq/be-fsstubs.c     | 4 ++--
 src/backend/utils/cache/relcache.c | 4 ++--
 src/backend/utils/mmgr/portalmem.c | 8 ++++----
 src/backend/utils/time/snapmgr.c   | 4 ++--
 7 files changed, 21 insertions(+), 19 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index f91392a3a49..40a2dc8d69b 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -594,9 +594,10 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		bloom_free(state->filter);
 	}
 
-	/* Be tidy: */
-	if (state->snapshot != InvalidSnapshot)
-		UnregisterSnapshot(state->snapshot);
+	/* Be tidy. Dont worry about snapshot being NULL here,
+	* as UnregisterSnapshot is ready to handle it. */
+	UnregisterSnapshot(state->snapshot);
+	
 	MemoryContextDelete(state->targetcontext);
 }
 
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index b7f10a1aed0..59607e2a160 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -616,8 +616,8 @@ systable_endscan(SysScanDesc sysscan)
 	else
 		table_endscan(sysscan->scan);
 
-	if (sysscan->snapshot)
-		UnregisterSnapshot(sysscan->snapshot);
+	/* Be tidy, unregister snaphot, if any. */
+	UnregisterSnapshot(sysscan->snapshot);
 
 	/*
 	 * Reset the bsysscan flag at the end of the systable scan.  See detailed
@@ -764,8 +764,9 @@ systable_endscan_ordered(SysScanDesc sysscan)
 
 	Assert(sysscan->irel);
 	index_endscan(sysscan->iscan);
-	if (sysscan->snapshot)
-		UnregisterSnapshot(sysscan->snapshot);
+	
+	/* Be tidy, unregister snaphot, if any. */
+	UnregisterSnapshot(sysscan->snapshot);
 
 	/*
 	 * Reset the bsysscan flag at the end of the systable scan.  See detailed
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 0492d92d23b..6727bcf8565 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -407,8 +407,8 @@ index_endscan(IndexScanDesc scan)
 	/* Release index refcount acquired by index_beginscan */
 	RelationDecrementReferenceCount(scan->indexRelation);
 
-	if (scan->xs_temp_snap)
-		UnregisterSnapshot(scan->xs_snapshot);
+	/* Be tidy, unregister snaphot, if any. */
+	UnregisterSnapshot(scan->xs_snapshot);
 
 	/* Release the scan data structure itself */
 	IndexScanEnd(scan);
diff --git a/src/backend/libpq/be-fsstubs.c b/src/backend/libpq/be-fsstubs.c
index e5a34c61931..3015683f51e 100644
--- a/src/backend/libpq/be-fsstubs.c
+++ b/src/backend/libpq/be-fsstubs.c
@@ -729,8 +729,8 @@ closeLOfd(int fd)
 	lobj = cookies[fd];
 	cookies[fd] = NULL;
 
-	if (lobj->snapshot)
-		UnregisterSnapshotFromOwner(lobj->snapshot,
+	/* Be tidy, unregister snaphot, if any. */
+	UnregisterSnapshotFromOwner(lobj->snapshot,
 									TopTransactionResourceOwner);
 	inv_close(lobj);
 }
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2d0cb7bcfd4..ef3707488b4 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -395,8 +395,8 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
 	/* all done */
 	systable_endscan(pg_class_scan);
 
-	if (snapshot)
-		UnregisterSnapshot(snapshot);
+	/* Be tidy, unregister snaphot, if any. */
+	UnregisterSnapshot(snapshot);
 
 	table_close(pg_class_desc, AccessShareLock);
 
diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c
index 1f2a423f38a..5d4c565b4af 100644
--- a/src/backend/utils/mmgr/portalmem.c
+++ b/src/backend/utils/mmgr/portalmem.c
@@ -525,8 +525,8 @@ PortalDrop(Portal portal, bool isTopCommit)
 	 */
 	if (portal->holdSnapshot)
 	{
-		if (portal->resowner)
-			UnregisterSnapshotFromOwner(portal->holdSnapshot,
+		/* Be tidy, unregister snaphot, if any. */
+		UnregisterSnapshotFromOwner(portal->holdSnapshot,
 										portal->resowner);
 		portal->holdSnapshot = NULL;
 	}
@@ -708,8 +708,8 @@ PreCommit_Portals(bool isPrepare)
 		{
 			if (portal->holdSnapshot)
 			{
-				if (portal->resowner)
-					UnregisterSnapshotFromOwner(portal->holdSnapshot,
+				/* Be tidy, unregister snaphot, if any. */
+				UnregisterSnapshotFromOwner(portal->holdSnapshot,
 												portal->resowner);
 				portal->holdSnapshot = NULL;
 			}
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 5af8326d5e8..1350b21efa7 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -865,7 +865,7 @@ RegisterSnapshotOnOwner(Snapshot snapshot, ResourceOwner owner)
 void
 UnregisterSnapshot(Snapshot snapshot)
 {
-	if (snapshot == NULL)
+	if (snapshot == InvalidSnapshot)
 		return;
 
 	UnregisterSnapshotFromOwner(snapshot, CurrentResourceOwner);
@@ -878,7 +878,7 @@ UnregisterSnapshot(Snapshot snapshot)
 void
 UnregisterSnapshotFromOwner(Snapshot snapshot, ResourceOwner owner)
 {
-	if (snapshot == NULL)
+	if (snapshot == InvalidSnapshot)
 		return;
 
 	ResourceOwnerForgetSnapshot(owner, snapshot);
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
  2025-12-19 11:51   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
@ 2025-12-22 02:59     ` Chao Li <[email protected]>
  1 sibling, 0 replies; 26+ messages in thread

From: Chao Li @ 2025-12-22 02:59 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: pgsql-hackers



> On Dec 19, 2025, at 19:51, Heikki Linnakangas <[email protected]> wrote:
> 
>> +/*
>> + * Generic union representing all kind of possible snapshots.  Some have
>> + * type-specific structs.
>> + */
>> +typedef union SnapshotData
>> +{
>> + SnapshotType snapshot_type; /* type of snapshot */
>> +
>> + MVCCSnapshotData mvcc;
>> + DirtySnapshotData dirty;
>> + HistoricMVCCSnapshotData historic_mvcc;
>> + NonVacuumableSnapshotData nonvacuumable;
>>  } SnapshotData;
>> ```
>> > And my big concern is here. This union definition looks unusual,
>> snapshot_type shares the same space with real snapshot bodies, so
>> that once snapshot is assigned to the union, that type info is lost,
>> there would be no way to decide what exact snapshot is stored in
>> SnapshotData.
> 
> Each of the structs, MVCCSnapshotData, DirtySnapshotData, etc., contain 'snapshot_type' as the first field, so it's always available.

Oh, I didn’t notice each of the structs contain “snapshot_type”, in that case, maybe we can just define SnapshotData as a structure with a single field “snapshot_type”, in the same way as “Node”.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/









^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
@ 2025-12-19 15:16 ` Andres Freund <[email protected]>
  2025-12-22 18:20   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  1 sibling, 1 reply; 26+ messages in thread

From: Andres Freund @ 2025-12-19 15:16 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: pgsql-hackers

Hi,

On 2025-12-19 01:30:05 +0200, Heikki Linnakangas wrote:
> Patch 0002: Split SnapshotData into separate structs for each kind of
> snapshot
>
> This implements the long-standing TODO and splits SnapshotData up into
> multiple structs. This makes it more clear which fields are used with which
> kind of snapshot. For example, we now have properly named fields for the XID
> arrays in historic snapshots. Previously, they abused the 'xip' and 'subxip'
> arrays to mean something different than what they mean in regular MVCC
> snapshots.
>
> This introduces some new casts between Snapshots and the new MVCCSnapshots.
> I struggled to decide which functions should use the new MVCCSnapshot type
> and which should continue to use Snapshot. It's a balancing act between code
> churn and where we want to have casts. For example, PushActiveSnapshot()
> takes a Snapshot argument, but assumes that it's an MVCC snapshot, so it
> really should take MVCCSnapshot. But most of its callers have a Snapshot
> rather than MVCCSnapshot. Another example: the executor assumes that it's
> dealing with MVCC snapshots, so it would be appropriate to use MVCCSnapshot
> for EState->es_snapshot. But it'd cause a lot code churn and casts
> elsewhere. I think the patch is a reasonable middle ground.

Generally I think this is good. A few points:
- I'd use a SnapshotBase type or such that then has the SnapshotType as a
  member. That way we can more cleanly add common fields if we need them

- I'd rename the fields for dirty snapshots, given how differently they're
  used for dirty snapshots, compared to anything else

- I'm somewhat doubtful that it's rigth to restict active snapshots to plain
  mvcc ones. Why is that the right thing? What if a historic mvcc snapshot is
  supposed to be pushed? What if we introduce different types of snapshots in
  the future, e.g. CSN ones on the standby?


> Patch 0004: Add an explicit 'valid' flag to MVCCSnapshotData
>
> Makes it more clear when the "static" snapshots returned by
> GetTransactionSnapshot(), GetLatestSnapshot() and GetCatalogSnapshot() are
> valid.

Given they're pointing to static memory location, won't that limit how much we
can rely on valid? If something else builds a snapshot a "wrong" reference to
a snapshot will suddenly appear valid again, no?



Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 26+ messages in thread

* Re: A few patches to clarify snapshot management, part 2
  2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
  2025-12-19 15:16 ` Re: A few patches to clarify snapshot management, part 2 Andres Freund <[email protected]>
@ 2025-12-22 18:20   ` Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 26+ messages in thread

From: Heikki Linnakangas @ 2025-12-22 18:20 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: pgsql-hackers; Chao Li <[email protected]>

On 19/12/2025 17:16, Andres Freund wrote:
> On 2025-12-19 01:30:05 +0200, Heikki Linnakangas wrote:
>> Patch 0002: Split SnapshotData into separate structs for each kind of
>> snapshot
>>
>> This implements the long-standing TODO and splits SnapshotData up into
>> multiple structs. This makes it more clear which fields are used with which
>> kind of snapshot. For example, we now have properly named fields for the XID
>> arrays in historic snapshots. Previously, they abused the 'xip' and 'subxip'
>> arrays to mean something different than what they mean in regular MVCC
>> snapshots.
>>
>> This introduces some new casts between Snapshots and the new MVCCSnapshots.
>> I struggled to decide which functions should use the new MVCCSnapshot type
>> and which should continue to use Snapshot. It's a balancing act between code
>> churn and where we want to have casts. For example, PushActiveSnapshot()
>> takes a Snapshot argument, but assumes that it's an MVCC snapshot, so it
>> really should take MVCCSnapshot. But most of its callers have a Snapshot
>> rather than MVCCSnapshot. Another example: the executor assumes that it's
>> dealing with MVCC snapshots, so it would be appropriate to use MVCCSnapshot
>> for EState->es_snapshot. But it'd cause a lot code churn and casts
>> elsewhere. I think the patch is a reasonable middle ground.
> 
> Generally I think this is good. A few points:
> - I'd use a SnapshotBase type or such that then has the SnapshotType as a
>    member. That way we can more cleanly add common fields if we need them

Ok. (Chao suggested that too)

> - I'd rename the fields for dirty snapshots, given how differently they're
>    used for dirty snapshots, compared to anything else

Hmm. The downside is a little more code churn, also in extensions that 
use DirtySnapshot. There are only few of them though, based on quick 
grepping I believe only pglogical is affected, so I think you're right. 
I renamed them to 'updating_xmin' and 'updating_xmax'.

> - I'm somewhat doubtful that it's rigth to restict active snapshots to plain
>    mvcc ones. Why is that the right thing? What if a historic mvcc snapshot is
>    supposed to be pushed? What if we introduce different types of snapshots in
>    the future, e.g. CSN ones on the standby?

Rats! Historic MVCC snapshots can indeed be pushed to the active stack. 
There is no core code that does that, but pglogical does it. This is 
precisely the case that Noah pointed out earlier [1], when I added an 
assertion against that in GetTransactionSnapshot(), and had to revert it.

Ok so we need to keep the support for that. That complicates the first 
patches in this series a little, but it's not too bad.

[1] 
https://www.postgresql.org/message-id/20250809222338.cc.nmisch%40google.com

>> Patch 0004: Add an explicit 'valid' flag to MVCCSnapshotData
>>
>> Makes it more clear when the "static" snapshots returned by
>> GetTransactionSnapshot(), GetLatestSnapshot() and GetCatalogSnapshot() are
>> valid.
> 
> Given they're pointing to static memory location, won't that limit how much we
> can rely on valid? If something else builds a snapshot a "wrong" reference to
> a snapshot will suddenly appear valid again, no?

It doesn't catch everything, but it catches more misuse than without it. 
For example, this would go unnoticed on 'master', but would hit the new 
assertion on the 'valid' flag:

snapshot = GetTransactionSnapshot();
PushActiveSnapshot(snapshot);

/* this invalidates the 'snapshot' and advances the advertised xmin */
PopActiveSnapshot();

/* This is wrong! 'snapshot' is invalid */
PushActiveSnapshot(snapshot);


But this mistake goes unnoticed with or without these patches:

snapshot = GetTransactionSnapshot();
PushActiveSnapshot(snapshot);

/* this invalidates the 'snapshot' and advances the advertised xmin */
PopActiveSnapshot();

other_snapshot = GetTransactionSnapshot();

/*
  * This "works", because 'snapshot' and 'other_snapshot' both point to
  * the same static SnapshotData. But it makes the *new* snapshot active,
  * not the first one!
  */
PushActiveSnapshot(snapshot);



Here's a new patch set. I only included the more mature patches for now, 
the rest need to be rebased.

The first patch is new. It adds sanity checks to a few places that 
previously assumed that they were dealing with a regular MVCC snapshot 
without checking.

Patch 0002 now accepts historic MVCC snapshots in PushActiveSnapshot(). 
I tested that it works with pglogical now, after updating pglogical 
renamed fields and such.

- Heikki


Attachments:

  [text/x-patch] v2-0001-Add-sanity-checks-against-using-non-MVCC-snapshot.patch (6.2K, ../../[email protected]/2-v2-0001-Add-sanity-checks-against-using-non-MVCC-snapshot.patch)
  download | inline diff:
From dabfbe45c63f165d94abb07814df536912c69a71 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 22 Dec 2025 19:12:19 +0200
Subject: [PATCH v2 1/5] Add sanity checks against using non-MVCC snapshots
 incorrectly

These functions assumed that the active snapshot is a regular MVCC
snapshot, but a historic MVCC snapshot can be pushed to the active
stack too. These functions shouldn't be called when doing logical
decoding, but better safe than sorry.
---
 contrib/amcheck/verify_heapam.c       | 6 +++++-
 contrib/amcheck/verify_nbtree.c       | 3 +++
 src/backend/access/transam/parallel.c | 5 +++++
 src/backend/catalog/pg_inherits.c     | 4 ++++
 src/backend/commands/async.c          | 2 ++
 src/backend/partitioning/partdesc.c   | 8 +++++++-
 src/backend/utils/adt/xid8funcs.c     | 2 ++
 src/backend/utils/time/snapmgr.c      | 2 ++
 8 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 130b3533463..2aae3119a7c 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -250,6 +250,7 @@ Datum
 verify_heapam(PG_FUNCTION_ARGS)
 {
 	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Snapshot	snap;
 	HeapCheckContext ctx;
 	Buffer		vmbuffer = InvalidBuffer;
 	Oid			relid;
@@ -310,7 +311,10 @@ verify_heapam(PG_FUNCTION_ARGS)
 	 * Any xmin newer than the xmin of our snapshot can't become all-visible
 	 * while we're running.
 	 */
-	ctx.safe_xmin = GetTransactionSnapshot()->xmin;
+	snap = GetTransactionSnapshot();
+	if (snap->snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "verify_heapam() cannot be used with a non-MVCC snapshot");
+	ctx.safe_xmin = snap->xmin;
 
 	/*
 	 * If we report corruption when not examining some individual attribute,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index f91392a3a49..f9eb1af3bcf 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -440,6 +440,9 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
 
+		if (state->snapshot->snapshot_type != SNAPSHOT_MVCC)
+			elog(ERROR, "cannot check index consistency with a non-MVCC snapshot");
+
 		/*
 		 * GetTransactionSnapshot() always acquires a new MVCC snapshot in
 		 * READ COMMITTED mode.  A new snapshot is guaranteed to have all the
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 642c61fc55c..423fce24248 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -229,6 +229,11 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	Snapshot	transaction_snapshot = GetTransactionSnapshot();
 	Snapshot	active_snapshot = GetActiveSnapshot();
 
+	if (transaction_snapshot->snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "cannot use parallel workers with non-MVCC transaction snapshot");
+	if (active_snapshot->snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "cannot use parallel workers with non-MVCC active snapshot");
+
 	/* We might be running in a very short-lived memory context. */
 	oldcontext = MemoryContextSwitchTo(TopTransactionContext);
 
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 929bb53b620..894656ec385 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -146,7 +146,11 @@ find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
 				Snapshot	snap;
 
 				xmin = HeapTupleHeaderGetXmin(inheritsTuple->t_data);
+
+				/* XXX: what should we do with a non-MVCC snapshot? */
 				snap = GetActiveSnapshot();
+				if (snap->snapshot_type != SNAPSHOT_MVCC)
+					elog(ERROR, "cannot look up partition information with a non-MVCC snapshot");
 
 				if (!XidInMVCCSnapshot(xmin, snap))
 				{
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index eb86402cae4..0de8abeda28 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -1992,6 +1992,8 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 	alignas(AsyncQueueEntry) char local_buf[QUEUE_PAGESIZE];
 	char	   *local_buf_end = local_buf;
 
+	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+
 	slotno = SimpleLruReadPage_ReadOnly(NotifyCtl, curpage,
 										InvalidTransactionId);
 	page_buffer = NotifyCtl->shared->page_buffer[slotno];
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 985f48fc34d..e4ea63f212f 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -102,7 +102,13 @@ RelationGetPartitionDesc(Relation rel, bool omit_detached)
 		Assert(TransactionIdIsValid(rel->rd_partdesc_nodetached_xmin));
 		activesnap = GetActiveSnapshot();
 
-		if (!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, activesnap))
+		/*
+		 * XXX: Not clear what we can do with a non-MVCC snapshot, so fall
+		 * through to let RelationBuildPartitionDesc() handle it.  (It
+		 * currently just throws an error.)
+		 */
+		if (activesnap->snapshot_type == SNAPSHOT_MVCC &&
+			!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, activesnap))
 			return rel->rd_partdesc_nodetached;
 	}
 
diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index 4b3f7a69b3b..7b38eec6345 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -379,6 +379,8 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	cur = GetActiveSnapshot();
 	if (cur == NULL)
 		elog(ERROR, "no active snapshot set");
+	if (cur->snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "pg_current_snapshot() cannot be used with a non-MVCC snapshot");
 
 	/* allocate */
 	nxip = cur->xcnt;
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 5af8326d5e8..f156845231b 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -1737,6 +1737,8 @@ SerializeSnapshot(Snapshot snapshot, char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 
+	if (snapshot->snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "cannot serialize non-MVCC snapshot");
 	Assert(snapshot->subxcnt >= 0);
 
 	/* Copy all required fields */
-- 
2.47.3



  [text/x-patch] v2-0002-Split-SnapshotData-into-separate-structs-for-each.patch (105.6K, ../../[email protected]/3-v2-0002-Split-SnapshotData-into-separate-structs-for-each.patch)
  download | inline diff:
From e2b3f0992785b2918ad82db128a99e8a0de2bb41 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Fri, 20 Dec 2024 00:36:33 +0200
Subject: [PATCH v2 2/5] Split SnapshotData into separate structs for each kind
 of snapshot

The SnapshotData fields were repurposed for different uses depending
the kind of snapshot. Split it into separate structs for different
kinds of snapshots, so that it is more clear which fields are used
with which snapshot kind, and the fields can have more descriptive
names.
---
 contrib/amcheck/verify_heapam.c               |   4 +-
 contrib/amcheck/verify_nbtree.c               |   4 +-
 src/backend/access/common/toast_internals.c   |   2 +-
 src/backend/access/heap/heapam.c              |   2 +-
 src/backend/access/heap/heapam_handler.c      |  19 +-
 src/backend/access/heap/heapam_visibility.c   |  55 +--
 src/backend/access/index/indexam.c            |  15 +-
 src/backend/access/nbtree/nbtinsert.c         |  10 +-
 src/backend/access/spgist/spgvacuum.c         |   7 +-
 src/backend/access/table/tableam.c            |  10 +-
 src/backend/access/transam/parallel.c         |  21 +-
 src/backend/catalog/pg_inherits.c             |   4 +-
 src/backend/commands/async.c                  |   4 +-
 src/backend/commands/indexcmds.c              |   4 +-
 src/backend/commands/tablecmds.c              |   2 +-
 src/backend/executor/execIndexing.c           |  16 +-
 src/backend/executor/execReplication.c        |  16 +-
 src/backend/partitioning/partdesc.c           |   4 +-
 src/backend/replication/logical/decode.c      |   2 +-
 src/backend/replication/logical/origin.c      |   4 +-
 .../replication/logical/reorderbuffer.c       | 115 +++---
 src/backend/replication/logical/snapbuild.c   | 118 ++++---
 src/backend/replication/walsender.c           |   2 +-
 src/backend/storage/ipc/procarray.c           |   6 +-
 src/backend/storage/lmgr/predicate.c          |  32 +-
 src/backend/utils/adt/xid8funcs.c             |  10 +-
 src/backend/utils/time/snapmgr.c              | 334 ++++++++++++------
 src/include/access/heapam.h                   |   2 +-
 src/include/access/relscan.h                  |   6 +-
 src/include/replication/reorderbuffer.h       |  12 +-
 src/include/replication/snapbuild.h           |   6 +-
 src/include/replication/snapbuild_internal.h  |   2 +-
 src/include/storage/predicate.h               |   4 +-
 src/include/storage/procarray.h               |   2 +-
 src/include/utils/snapmgr.h                   |  40 +--
 src/include/utils/snapshot.h                  | 187 +++++++---
 src/tools/pgindent/typedefs.list              |   5 +
 37 files changed, 645 insertions(+), 443 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 2aae3119a7c..611a4a6687c 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -312,9 +312,9 @@ verify_heapam(PG_FUNCTION_ARGS)
 	 * while we're running.
 	 */
 	snap = GetTransactionSnapshot();
-	if (snap->snapshot_type != SNAPSHOT_MVCC)
+	if (snap->base.snapshot_type != SNAPSHOT_MVCC)
 		elog(ERROR, "verify_heapam() cannot be used with a non-MVCC snapshot");
-	ctx.safe_xmin = snap->xmin;
+	ctx.safe_xmin = snap->mvcc.xmin;
 
 	/*
 	 * If we report corruption when not examining some individual attribute,
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index f9eb1af3bcf..dc291376654 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -440,7 +440,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		state->snapshot = RegisterSnapshot(GetTransactionSnapshot());
 
-		if (state->snapshot->snapshot_type != SNAPSHOT_MVCC)
+		if (state->snapshot->base.snapshot_type != SNAPSHOT_MVCC)
 			elog(ERROR, "cannot check index consistency with a non-MVCC snapshot");
 
 		/*
@@ -457,7 +457,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace,
 		 */
 		if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin &&
 			!TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data),
-								   state->snapshot->xmin))
+								   state->snapshot->mvcc.xmin))
 			ereport(ERROR,
 					errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 					errmsg("index \"%s\" cannot be verified using transaction snapshot",
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index d06af82de15..6b6e4ff34bd 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -643,5 +643,5 @@ get_toast_snapshot(void)
 	if (!HaveRegisteredOrActiveSnapshot())
 		elog(ERROR, "cannot fetch toast data without an active snapshot");
 
-	return &SnapshotToastData;
+	return (Snapshot) &SnapshotToastData;
 }
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6daf4a87dec..a40354a07a2 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -616,7 +616,7 @@ heap_prepare_pagescan(TableScanDesc sscan)
 	 * full page write. Until we can prove that beyond doubt, let's check each
 	 * tuple for visibility the hard way.
 	 */
-	all_visible = PageIsAllVisible(page) && !snapshot->takenDuringRecovery;
+	all_visible = PageIsAllVisible(page) && !IsSnapshotTakenDuringRecovery(snapshot);
 	check_serializable =
 		CheckForSerializableConflictOutNeeded(scan->rs_base.rs_rd, snapshot);
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index dd4fe6bf62f..2faaf2be35a 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -390,7 +390,7 @@ tuple_lock_retry:
 
 		if (!ItemPointerEquals(&tmfd->ctid, &tuple->t_self))
 		{
-			SnapshotData SnapshotDirty;
+			DirtySnapshotData SnapshotDirty;
 			TransactionId priorXmax;
 
 			/* it was updated, so look at the updated version */
@@ -415,7 +415,7 @@ tuple_lock_retry:
 							 errmsg("tuple to be locked was already moved to another partition due to concurrent update")));
 
 				tuple->t_self = *tid;
-				if (heap_fetch(relation, &SnapshotDirty, tuple, &buffer, true))
+				if (heap_fetch(relation, (Snapshot) &SnapshotDirty, tuple, &buffer, true))
 				{
 					/*
 					 * If xmin isn't what we're expecting, the slot must have
@@ -435,11 +435,11 @@ tuple_lock_retry:
 					}
 
 					/* otherwise xmin should not be dirty... */
-					if (TransactionIdIsValid(SnapshotDirty.xmin))
+					if (TransactionIdIsValid(SnapshotDirty.updating_xmin))
 						ereport(ERROR,
 								(errcode(ERRCODE_DATA_CORRUPTED),
 								 errmsg_internal("t_xmin %u is uncommitted in tuple (%u,%u) to be updated in table \"%s\"",
-												 SnapshotDirty.xmin,
+												 SnapshotDirty.updating_xmin,
 												 ItemPointerGetBlockNumber(&tuple->t_self),
 												 ItemPointerGetOffsetNumber(&tuple->t_self),
 												 RelationGetRelationName(relation))));
@@ -448,23 +448,23 @@ tuple_lock_retry:
 					 * If tuple is being updated by other transaction then we
 					 * have to wait for its commit/abort, or die trying.
 					 */
-					if (TransactionIdIsValid(SnapshotDirty.xmax))
+					if (TransactionIdIsValid(SnapshotDirty.updating_xmax))
 					{
 						ReleaseBuffer(buffer);
 						switch (wait_policy)
 						{
 							case LockWaitBlock:
-								XactLockTableWait(SnapshotDirty.xmax,
+								XactLockTableWait(SnapshotDirty.updating_xmax,
 												  relation, &tuple->t_self,
 												  XLTW_FetchUpdated);
 								break;
 							case LockWaitSkip:
-								if (!ConditionalXactLockTableWait(SnapshotDirty.xmax, false))
+								if (!ConditionalXactLockTableWait(SnapshotDirty.updating_xmax, false))
 									/* skip instead of waiting */
 									return TM_WouldBlock;
 								break;
 							case LockWaitError:
-								if (!ConditionalXactLockTableWait(SnapshotDirty.xmax, log_lock_failures))
+								if (!ConditionalXactLockTableWait(SnapshotDirty.updating_xmax, log_lock_failures))
 									ereport(ERROR,
 											(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 											 errmsg("could not obtain lock on row in relation \"%s\"",
@@ -2281,8 +2281,7 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
 		LockBuffer(hscan->rs_cbuf, BUFFER_LOCK_SHARE);
 
 	page = BufferGetPage(hscan->rs_cbuf);
-	all_visible = PageIsAllVisible(page) &&
-		!scan->rs_snapshot->takenDuringRecovery;
+	all_visible = PageIsAllVisible(page) && !IsSnapshotTakenDuringRecovery(scan->rs_snapshot);
 	maxoffset = PageGetMaxOffsetNumber(page);
 
 	for (;;)
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index bf899c2d2c6..4462e91faec 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -669,16 +669,16 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid,
  *
  * A special hack is that the passed-in snapshot struct is used as an
  * output argument to return the xids of concurrent xacts that affected the
- * tuple.  snapshot->xmin is set to the tuple's xmin if that is another
- * transaction that's still in progress; or to InvalidTransactionId if the
- * tuple's xmin is committed good, committed dead, or my own xact.
- * Similarly for snapshot->xmax and the tuple's xmax.  If the tuple was
- * inserted speculatively, meaning that the inserter might still back down
- * on the insertion without aborting the whole transaction, the associated
- * token is also returned in snapshot->speculativeToken.
+ * tuple.  snapshot->updating_xmin is set to the tuple's xmin if that is
+ * another transaction that's still in progress; or to InvalidTransactionId
+ * if the tuple's xmin is committed good, committed dead, or my own xact.
+ * Similarly for snapshot->updating_xmax and the tuple's xmax.  If the tuple
+ * was inserted speculatively, meaning that the inserter might still back
+ * down on the insertion without aborting the whole transaction, the
+ * associated token is also returned in snapshot->speculative_token.
  */
 static bool
-HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesDirty(HeapTuple htup, DirtySnapshotData *snapshot,
 						Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -686,8 +686,9 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
 	Assert(ItemPointerIsValid(&htup->t_self));
 	Assert(htup->t_tableOid != InvalidOid);
 
-	snapshot->xmin = snapshot->xmax = InvalidTransactionId;
-	snapshot->speculativeToken = 0;
+	snapshot->updating_xmin = InvalidTransactionId;
+	snapshot->updating_xmax = InvalidTransactionId;
+	snapshot->speculative_token = 0;
 
 	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
@@ -740,13 +741,13 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
 			 */
 			if (HeapTupleHeaderIsSpeculative(tuple))
 			{
-				snapshot->speculativeToken =
+				snapshot->speculative_token =
 					HeapTupleHeaderGetSpeculativeToken(tuple);
 
-				Assert(snapshot->speculativeToken != 0);
+				Assert(snapshot->speculative_token != 0);
 			}
 
-			snapshot->xmin = HeapTupleHeaderGetRawXmin(tuple);
+			snapshot->updating_xmin = HeapTupleHeaderGetRawXmin(tuple);
 			/* XXX shouldn't we fall through to look at xmax? */
 			return true;		/* in insertion by other */
 		}
@@ -790,7 +791,7 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
 			return false;
 		if (TransactionIdIsInProgress(xmax))
 		{
-			snapshot->xmax = xmax;
+			snapshot->updating_xmax = xmax;
 			return true;
 		}
 		if (TransactionIdDidCommit(xmax))
@@ -809,7 +810,7 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
 	if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmax(tuple)))
 	{
 		if (!HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
-			snapshot->xmax = HeapTupleHeaderGetRawXmax(tuple);
+			snapshot->updating_xmax = HeapTupleHeaderGetRawXmax(tuple);
 		return true;
 	}
 
@@ -858,7 +859,7 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot,
  * and more contention on ProcArrayLock.
  */
 static bool
-HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesMVCC(HeapTuple htup, MVCCSnapshot snapshot,
 					   Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -1264,7 +1265,7 @@ HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *de
  *	snapshot->vistest must have been set up with the horizon to use.
  */
 static bool
-HeapTupleSatisfiesNonVacuumable(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesNonVacuumable(HeapTuple htup, NonVacuumableSnapshotData *snapshot,
 								Buffer buffer)
 {
 	TransactionId dead_after = InvalidTransactionId;
@@ -1422,7 +1423,7 @@ TransactionIdInArray(TransactionId xid, TransactionId *xip, Size num)
  * complicated than when dealing "only" with the present.
  */
 static bool
-HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
+HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, HistoricMVCCSnapshot snapshot,
 							   Buffer buffer)
 {
 	HeapTupleHeader tuple = htup->t_data;
@@ -1439,7 +1440,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 		return false;
 	}
 	/* check if it's one of our txids, toplevel is also in there */
-	else if (TransactionIdInArray(xmin, snapshot->subxip, snapshot->subxcnt))
+	else if (TransactionIdInArray(xmin, snapshot->curxip, snapshot->curxcnt))
 	{
 		bool		resolved;
 		CommandId	cmin = HeapTupleHeaderGetRawCommandId(tuple);
@@ -1498,7 +1499,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 		return false;
 	}
 	/* check if it's a committed transaction in [xmin, xmax) */
-	else if (TransactionIdInArray(xmin, snapshot->xip, snapshot->xcnt))
+	else if (TransactionIdInArray(xmin, snapshot->committed_xids, snapshot->xcnt))
 	{
 		/* fall through */
 	}
@@ -1531,7 +1532,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 	}
 
 	/* check if it's one of our txids, toplevel is also in there */
-	if (TransactionIdInArray(xmax, snapshot->subxip, snapshot->subxcnt))
+	if (TransactionIdInArray(xmax, snapshot->curxip, snapshot->curxcnt))
 	{
 		bool		resolved;
 		CommandId	cmin;
@@ -1584,7 +1585,7 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 	else if (TransactionIdFollowsOrEquals(xmax, snapshot->xmax))
 		return true;
 	/* xmax is between [xmin, xmax), check known committed array */
-	else if (TransactionIdInArray(xmax, snapshot->xip, snapshot->xcnt))
+	else if (TransactionIdInArray(xmax, snapshot->committed_xids, snapshot->xcnt))
 		return false;
 	/* xmax is between [xmin, xmax), but known not to have committed yet */
 	else
@@ -1604,10 +1605,10 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
 bool
 HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer)
 {
-	switch (snapshot->snapshot_type)
+	switch (snapshot->base.snapshot_type)
 	{
 		case SNAPSHOT_MVCC:
-			return HeapTupleSatisfiesMVCC(htup, snapshot, buffer);
+			return HeapTupleSatisfiesMVCC(htup, &snapshot->mvcc, buffer);
 		case SNAPSHOT_SELF:
 			return HeapTupleSatisfiesSelf(htup, snapshot, buffer);
 		case SNAPSHOT_ANY:
@@ -1615,11 +1616,11 @@ HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer)
 		case SNAPSHOT_TOAST:
 			return HeapTupleSatisfiesToast(htup, snapshot, buffer);
 		case SNAPSHOT_DIRTY:
-			return HeapTupleSatisfiesDirty(htup, snapshot, buffer);
+			return HeapTupleSatisfiesDirty(htup, &snapshot->dirty, buffer);
 		case SNAPSHOT_HISTORIC_MVCC:
-			return HeapTupleSatisfiesHistoricMVCC(htup, snapshot, buffer);
+			return HeapTupleSatisfiesHistoricMVCC(htup, &snapshot->historic_mvcc, buffer);
 		case SNAPSHOT_NON_VACUUMABLE:
-			return HeapTupleSatisfiesNonVacuumable(htup, snapshot, buffer);
+			return HeapTupleSatisfiesNonVacuumable(htup, &snapshot->nonvacuumable, buffer);
 	}
 
 	return false;				/* keep compiler quiet */
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 0492d92d23b..05c048ecac7 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -479,7 +479,7 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys,
 	RELATION_CHECKS;
 
 	nbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data);
-	nbytes = add_size(nbytes, EstimateSnapshotSpace(snapshot));
+	nbytes = add_size(nbytes, EstimateSnapshotSpace(&snapshot->mvcc));
 	nbytes = MAXALIGN(nbytes);
 
 	if (instrument)
@@ -524,20 +524,25 @@ index_parallelscan_initialize(Relation heapRelation, Relation indexRelation,
 							  ParallelIndexScanDesc target)
 {
 	Size		offset;
+	MVCCSnapshot mvcc_snapshot;
 
 	Assert(instrument || parallel_aware);
 
 	RELATION_CHECKS;
 
+	if (snapshot->base.snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "cannot perform parallel scan with non-MVCC snapshot");
+	mvcc_snapshot = &snapshot->mvcc;
+
 	offset = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data),
-					  EstimateSnapshotSpace(snapshot));
+					  EstimateSnapshotSpace(mvcc_snapshot));
 	offset = MAXALIGN(offset);
 
 	target->ps_locator = heapRelation->rd_locator;
 	target->ps_indexlocator = indexRelation->rd_locator;
 	target->ps_offset_ins = 0;
 	target->ps_offset_am = 0;
-	SerializeSnapshot(snapshot, target->ps_snapshot_data);
+	SerializeSnapshot(mvcc_snapshot, target->ps_snapshot_data);
 
 	if (instrument)
 	{
@@ -601,8 +606,8 @@ index_beginscan_parallel(Relation heaprel, Relation indexrel,
 	Assert(RelFileLocatorEquals(heaprel->rd_locator, pscan->ps_locator));
 	Assert(RelFileLocatorEquals(indexrel->rd_locator, pscan->ps_indexlocator));
 
-	snapshot = RestoreSnapshot(pscan->ps_snapshot_data);
-	RegisterSnapshot(snapshot);
+	snapshot = (Snapshot) RestoreSnapshot(pscan->ps_snapshot_data);
+	snapshot = RegisterSnapshot(snapshot);
 	scan = index_beginscan_internal(indexrel, nkeys, norderbys, snapshot,
 									pscan, true);
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 031eb76ba8c..59b3e1264e8 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -415,7 +415,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 	IndexTuple	curitup = NULL;
 	ItemId		curitemid = NULL;
 	BTScanInsert itup_key = insertstate->itup_key;
-	SnapshotData SnapshotDirty;
+	DirtySnapshotData SnapshotDirty;
 	OffsetNumber offset;
 	OffsetNumber maxoff;
 	Page		page;
@@ -560,7 +560,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 				 * index entry for the entire chain.
 				 */
 				else if (table_index_fetch_tuple_check(heapRel, &htid,
-													   &SnapshotDirty,
+													   (Snapshot) &SnapshotDirty,
 													   &all_dead))
 				{
 					TransactionId xwait;
@@ -585,15 +585,15 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 					 * If this tuple is being updated by other transaction
 					 * then we have to wait for its commit/abort.
 					 */
-					xwait = (TransactionIdIsValid(SnapshotDirty.xmin)) ?
-						SnapshotDirty.xmin : SnapshotDirty.xmax;
+					xwait = (TransactionIdIsValid(SnapshotDirty.updating_xmin)) ?
+						SnapshotDirty.updating_xmin : SnapshotDirty.updating_xmax;
 
 					if (TransactionIdIsValid(xwait))
 					{
 						if (nbuf != InvalidBuffer)
 							_bt_relbuf(rel, nbuf);
 						/* Tell _bt_doinsert to wait... */
-						*speculativeToken = SnapshotDirty.speculativeToken;
+						*speculativeToken = SnapshotDirty.speculative_token;
 						/* Caller releases lock on buf immediately */
 						insertstate->bounds_valid = false;
 						return xwait;
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index cb5671c1a4e..13bfd72cb1d 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -804,13 +804,18 @@ spgvacuumscan(spgBulkDeleteState *bds)
 	BlockNumber num_pages;
 	BlockRangeReadStreamPrivate p;
 	ReadStream *stream = NULL;
+	Snapshot	active_snap;
 
 	/* Finish setting up spgBulkDeleteState */
 	initSpGistState(&bds->spgstate, index);
 	bds->pendingList = NULL;
-	bds->myXmin = GetActiveSnapshot()->xmin;
 	bds->lastFilledBlock = SPGIST_LAST_FIXED_BLKNO;
 
+	active_snap = GetActiveSnapshot();
+	if (active_snap->base.snapshot_type != SNAPSHOT_MVCC)
+		elog(ERROR, "unexpected non-MVCC snapshot in vacuum");
+	bds->myXmin = active_snap->mvcc.xmin;
+
 	/*
 	 * Reset counts that will be incremented during the scan; needed in case
 	 * of multiple scans during a single VACUUM command
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 73ebc01a08f..fde28accfd3 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -133,7 +133,7 @@ table_parallelscan_estimate(Relation rel, Snapshot snapshot)
 	Size		sz = 0;
 
 	if (IsMVCCSnapshot(snapshot))
-		sz = add_size(sz, EstimateSnapshotSpace(snapshot));
+		sz = add_size(sz, EstimateSnapshotSpace((MVCCSnapshot) snapshot));
 	else
 		Assert(snapshot == SnapshotAny);
 
@@ -152,7 +152,7 @@ table_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan,
 
 	if (IsMVCCSnapshot(snapshot))
 	{
-		SerializeSnapshot(snapshot, (char *) pscan + pscan->phs_snapshot_off);
+		SerializeSnapshot((MVCCSnapshot) snapshot, (char *) pscan + pscan->phs_snapshot_off);
 		pscan->phs_snapshot_any = false;
 	}
 	else
@@ -174,8 +174,8 @@ table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan)
 	if (!pscan->phs_snapshot_any)
 	{
 		/* Snapshot was serialized -- restore it */
-		snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
-		RegisterSnapshot(snapshot);
+		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
+		snapshot = RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
 	else
@@ -204,7 +204,7 @@ table_beginscan_parallel_tidrange(Relation relation,
 	if (!pscan->phs_snapshot_any)
 	{
 		/* Snapshot was serialized -- restore it */
-		snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
+		snapshot = (Snapshot) RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off);
 		RegisterSnapshot(snapshot);
 		flags |= SO_TEMP_SNAPSHOT;
 	}
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 423fce24248..faea28c041e 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -229,9 +229,9 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	Snapshot	transaction_snapshot = GetTransactionSnapshot();
 	Snapshot	active_snapshot = GetActiveSnapshot();
 
-	if (transaction_snapshot->snapshot_type != SNAPSHOT_MVCC)
+	if (transaction_snapshot->base.snapshot_type != SNAPSHOT_MVCC)
 		elog(ERROR, "cannot use parallel workers with non-MVCC transaction snapshot");
-	if (active_snapshot->snapshot_type != SNAPSHOT_MVCC)
+	if (active_snapshot->base.snapshot_type != SNAPSHOT_MVCC)
 		elog(ERROR, "cannot use parallel workers with non-MVCC active snapshot");
 
 	/* We might be running in a very short-lived memory context. */
@@ -284,10 +284,10 @@ InitializeParallelDSM(ParallelContext *pcxt)
 		shm_toc_estimate_chunk(&pcxt->estimator, combocidlen);
 		if (IsolationUsesXactSnapshot())
 		{
-			tsnaplen = EstimateSnapshotSpace(transaction_snapshot);
+			tsnaplen = EstimateSnapshotSpace((MVCCSnapshot) transaction_snapshot);
 			shm_toc_estimate_chunk(&pcxt->estimator, tsnaplen);
 		}
-		asnaplen = EstimateSnapshotSpace(active_snapshot);
+		asnaplen = EstimateSnapshotSpace((MVCCSnapshot) active_snapshot);
 		shm_toc_estimate_chunk(&pcxt->estimator, asnaplen);
 		tstatelen = EstimateTransactionStateSpace();
 		shm_toc_estimate_chunk(&pcxt->estimator, tstatelen);
@@ -406,14 +406,14 @@ InitializeParallelDSM(ParallelContext *pcxt)
 		if (IsolationUsesXactSnapshot())
 		{
 			tsnapspace = shm_toc_allocate(pcxt->toc, tsnaplen);
-			SerializeSnapshot(transaction_snapshot, tsnapspace);
+			SerializeSnapshot((MVCCSnapshot) transaction_snapshot, tsnapspace);
 			shm_toc_insert(pcxt->toc, PARALLEL_KEY_TRANSACTION_SNAPSHOT,
 						   tsnapspace);
 		}
 
 		/* Serialize the active snapshot. */
 		asnapspace = shm_toc_allocate(pcxt->toc, asnaplen);
-		SerializeSnapshot(active_snapshot, asnapspace);
+		SerializeSnapshot((MVCCSnapshot) active_snapshot, asnapspace);
 		shm_toc_insert(pcxt->toc, PARALLEL_KEY_ACTIVE_SNAPSHOT, asnapspace);
 
 		/* Provide the handle for per-session segment. */
@@ -1325,8 +1325,8 @@ ParallelWorkerMain(Datum main_arg)
 	char	   *uncommittedenumsspace;
 	char	   *clientconninfospace;
 	char	   *session_dsm_handle_space;
-	Snapshot	tsnapshot;
-	Snapshot	asnapshot;
+	MVCCSnapshot tsnapshot;
+	MVCCSnapshot asnapshot;
 
 	/* Set flag to indicate that we're initializing a parallel worker. */
 	InitializingParallelWorker = true;
@@ -1507,9 +1507,8 @@ ParallelWorkerMain(Datum main_arg)
 	tsnapspace = shm_toc_lookup(toc, PARALLEL_KEY_TRANSACTION_SNAPSHOT, true);
 	asnapshot = RestoreSnapshot(asnapspace);
 	tsnapshot = tsnapspace ? RestoreSnapshot(tsnapspace) : asnapshot;
-	RestoreTransactionSnapshot(tsnapshot,
-							   fps->parallel_leader_pgproc);
-	PushActiveSnapshot(asnapshot);
+	RestoreTransactionSnapshot(tsnapshot, fps->parallel_leader_pgproc);
+	PushActiveSnapshot((Snapshot) asnapshot);
 
 	/*
 	 * We've changed which tuples we can see, and must therefore invalidate
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 894656ec385..d8c9590c62e 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -149,10 +149,10 @@ find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
 
 				/* XXX: what should we do with a non-MVCC snapshot? */
 				snap = GetActiveSnapshot();
-				if (snap->snapshot_type != SNAPSHOT_MVCC)
+				if (snap->base.snapshot_type != SNAPSHOT_MVCC)
 					elog(ERROR, "cannot look up partition information with a non-MVCC snapshot");
 
-				if (!XidInMVCCSnapshot(xmin, snap))
+				if (!XidInMVCCSnapshot(xmin, (MVCCSnapshot) snap))
 				{
 					if (detached_xmin)
 					{
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index 0de8abeda28..9ce54d931da 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -1992,7 +1992,7 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 	alignas(AsyncQueueEntry) char local_buf[QUEUE_PAGESIZE];
 	char	   *local_buf_end = local_buf;
 
-	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+	Assert(snapshot->base.snapshot_type == SNAPSHOT_MVCC);
 
 	slotno = SimpleLruReadPage_ReadOnly(NotifyCtl, curpage,
 										InvalidTransactionId);
@@ -2018,7 +2018,7 @@ asyncQueueProcessPageEntries(QueuePosition *current,
 		/* Ignore messages destined for other databases */
 		if (qe->dboid == MyDatabaseId)
 		{
-			if (XidInMVCCSnapshot(qe->xid, snapshot))
+			if (XidInMVCCSnapshot(qe->xid, (MVCCSnapshot) snapshot))
 			{
 				/*
 				 * The source transaction is still in progress, so we can't
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d9cccb6ac18..76a94172200 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1760,7 +1760,7 @@ DefineIndex(Oid tableId,
 	 * they must wait for.  But first, save the snapshot's xmin to use as
 	 * limitXmin for GetCurrentVirtualXIDs().
 	 */
-	limitXmin = snapshot->xmin;
+	limitXmin = snapshot->mvcc.xmin;
 
 	PopActiveSnapshot();
 	UnregisterSnapshot(snapshot);
@@ -4190,7 +4190,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
 		 * We can now do away with our active snapshot, we still need to save
 		 * the xmin limit to wait for older snapshots.
 		 */
-		limitXmin = snapshot->xmin;
+		limitXmin = snapshot->mvcc.xmin;
 
 		PopActiveSnapshot();
 		UnregisterSnapshot(snapshot);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..b2d52f610f5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -21474,7 +21474,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
 	 * all such queries are complete (otherwise we would present them with an
 	 * inconsistent view of catalogs).
 	 */
-	WaitForOlderSnapshots(snap->xmin, false);
+	WaitForOlderSnapshots(snap->mvcc.xmin, false);
 
 	DetachPartitionFinalize(rel, partRel, true, InvalidOid);
 
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 0b3a31f1703..f1c1d7d56b0 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -717,7 +717,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 	int			indnkeyatts = IndexRelationGetNumberOfKeyAttributes(index);
 	IndexScanDesc index_scan;
 	ScanKeyData scankeys[INDEX_MAX_KEYS];
-	SnapshotData DirtySnapshot;
+	DirtySnapshotData DirtySnapshot;
 	int			i;
 	bool		conflict;
 	bool		found_self;
@@ -816,7 +816,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 retry:
 	conflict = false;
 	found_self = false;
-	index_scan = index_beginscan(heap, index, &DirtySnapshot, NULL, indnkeyatts, 0);
+	index_scan = index_beginscan(heap, index, (Snapshot) &DirtySnapshot, NULL, indnkeyatts, 0);
 	index_rescan(index_scan, scankeys, indnkeyatts, NULL, 0);
 
 	while (index_getnext_slot(index_scan, ForwardScanDirection, existing_slot))
@@ -870,21 +870,21 @@ retry:
 		 * happen often enough to be worth trying harder, and anyway we don't
 		 * want to hold any index internal locks while waiting.
 		 */
-		xwait = TransactionIdIsValid(DirtySnapshot.xmin) ?
-			DirtySnapshot.xmin : DirtySnapshot.xmax;
+		xwait = TransactionIdIsValid(DirtySnapshot.updating_xmin) ?
+			DirtySnapshot.updating_xmin : DirtySnapshot.updating_xmax;
 
 		if (TransactionIdIsValid(xwait) &&
 			(waitMode == CEOUC_WAIT ||
 			 (waitMode == CEOUC_LIVELOCK_PREVENTING_WAIT &&
-			  DirtySnapshot.speculativeToken &&
+			  DirtySnapshot.speculative_token &&
 			  TransactionIdPrecedes(GetCurrentTransactionId(), xwait))))
 		{
 			reason_wait = indexInfo->ii_ExclusionOps ?
 				XLTW_RecheckExclusionConstr : XLTW_InsertIndex;
 			index_endscan(index_scan);
-			if (DirtySnapshot.speculativeToken)
-				SpeculativeInsertionWait(DirtySnapshot.xmin,
-										 DirtySnapshot.speculativeToken);
+			if (DirtySnapshot.speculative_token)
+				SpeculativeInsertionWait(DirtySnapshot.updating_xmin,
+										 DirtySnapshot.speculative_token);
 			else
 				XactLockTableWait(xwait, heap,
 								  &existing_slot->tts_tid, reason_wait);
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 860f79f9cc1..c4ad0034884 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -186,7 +186,7 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 	ScanKeyData skey[INDEX_MAX_KEYS];
 	int			skey_attoff;
 	IndexScanDesc scan;
-	SnapshotData snap;
+	DirtySnapshotData snap;
 	TransactionId xwait;
 	Relation	idxrel;
 	bool		found;
@@ -204,7 +204,7 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 	skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot);
 
 	/* Start an index scan. */
-	scan = index_beginscan(rel, idxrel, &snap, NULL, skey_attoff, 0);
+	scan = index_beginscan(rel, idxrel, (Snapshot) &snap, NULL, skey_attoff, 0);
 
 retry:
 	found = false;
@@ -229,8 +229,8 @@ retry:
 
 		ExecMaterializeSlot(outslot);
 
-		xwait = TransactionIdIsValid(snap.xmin) ?
-			snap.xmin : snap.xmax;
+		xwait = TransactionIdIsValid(snap.updating_xmin) ?
+			snap.updating_xmin : snap.updating_xmax;
 
 		/*
 		 * If the tuple is locked, wait for locking transaction to finish and
@@ -370,7 +370,7 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode,
 {
 	TupleTableSlot *scanslot;
 	TableScanDesc scan;
-	SnapshotData snap;
+	DirtySnapshotData snap;
 	TypeCacheEntry **eq;
 	TransactionId xwait;
 	bool		found;
@@ -382,7 +382,7 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode,
 
 	/* Start a heap scan. */
 	InitDirtySnapshot(snap);
-	scan = table_beginscan(rel, &snap, 0, NULL);
+	scan = table_beginscan(rel, (Snapshot) &snap, 0, NULL);
 	scanslot = table_slot_create(rel, NULL);
 
 retry:
@@ -399,8 +399,8 @@ retry:
 		found = true;
 		ExecCopySlot(outslot, scanslot);
 
-		xwait = TransactionIdIsValid(snap.xmin) ?
-			snap.xmin : snap.xmax;
+		xwait = TransactionIdIsValid(snap.updating_xmin) ?
+			snap.updating_xmin : snap.updating_xmax;
 
 		/*
 		 * If the tuple is locked, wait for locking transaction to finish and
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index e4ea63f212f..039642433a6 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -107,8 +107,8 @@ RelationGetPartitionDesc(Relation rel, bool omit_detached)
 		 * through to let RelationBuildPartitionDesc() handle it.  (It
 		 * currently just throws an error.)
 		 */
-		if (activesnap->snapshot_type == SNAPSHOT_MVCC &&
-			!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, activesnap))
+		if (activesnap->base.snapshot_type == SNAPSHOT_MVCC &&
+			!XidInMVCCSnapshot(rel->rd_partdesc_nodetached_xmin, &activesnap->mvcc))
 			return rel->rd_partdesc_nodetached;
 	}
 
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 5e15cb1825e..4730d935bac 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -590,7 +590,7 @@ logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	TransactionId xid = XLogRecGetXid(r);
 	uint8		info = XLogRecGetInfo(r) & ~XLR_INFO_MASK;
 	RepOriginId origin_id = XLogRecGetOrigin(r);
-	Snapshot	snapshot = NULL;
+	HistoricMVCCSnapshot snapshot = NULL;
 	xl_logical_message *message;
 
 	if (info != XLOG_LOGICAL_MESSAGE)
diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c
index 2380f369578..2d3ee773f9d 100644
--- a/src/backend/replication/logical/origin.c
+++ b/src/backend/replication/logical/origin.c
@@ -260,7 +260,7 @@ replorigin_create(const char *roname)
 	HeapTuple	tuple = NULL;
 	Relation	rel;
 	Datum		roname_d;
-	SnapshotData SnapshotDirty;
+	DirtySnapshotData SnapshotDirty;
 	SysScanDesc scan;
 	ScanKeyData key;
 
@@ -325,7 +325,7 @@ replorigin_create(const char *roname)
 
 		scan = systable_beginscan(rel, ReplicationOriginIdentIndex,
 								  true /* indexOK */ ,
-								  &SnapshotDirty,
+								  (Snapshot) &SnapshotDirty,
 								  1, &key);
 
 		collides = HeapTupleIsValid(systable_getnext(scan));
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index f18c6fb52b5..ba032f18c3f 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -280,9 +280,9 @@ static void ReorderBufferSerializedPath(char *path, ReplicationSlot *slot,
 										TransactionId xid, XLogSegNo segno);
 static int	ReorderBufferTXNSizeCompare(const pairingheap_node *a, const pairingheap_node *b, void *arg);
 
-static void ReorderBufferFreeSnap(ReorderBuffer *rb, Snapshot snap);
-static Snapshot ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
-									  ReorderBufferTXN *txn, CommandId cid);
+static void ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap);
+static HistoricMVCCSnapshot ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
+												  ReorderBufferTXN *txn, CommandId cid);
 
 /*
  * ---------------------------------------
@@ -871,7 +871,7 @@ ReorderBufferQueueChange(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn,
  */
 void
 ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
-						  Snapshot snap, XLogRecPtr lsn,
+						  HistoricMVCCSnapshot snap, XLogRecPtr lsn,
 						  bool transactional, const char *prefix,
 						  Size message_size, const char *message)
 {
@@ -905,7 +905,7 @@ ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
 	else
 	{
 		ReorderBufferTXN *txn = NULL;
-		volatile Snapshot snapshot_now = snap;
+		volatile	HistoricMVCCSnapshot snapshot_now = snap;
 
 		/* Non-transactional changes require a valid snapshot. */
 		Assert(snapshot_now);
@@ -1905,55 +1905,56 @@ ReorderBufferBuildTupleCidHash(ReorderBuffer *rb, ReorderBufferTXN *txn)
  * that catalog modifying transactions can look into intermediate catalog
  * states.
  */
-static Snapshot
-ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
+static HistoricMVCCSnapshot
+ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 					  ReorderBufferTXN *txn, CommandId cid)
 {
-	Snapshot	snap;
+	HistoricMVCCSnapshot snap;
 	dlist_iter	iter;
 	int			i = 0;
 	Size		size;
 
-	size = sizeof(SnapshotData) +
+	size = sizeof(HistoricMVCCSnapshotData) +
 		sizeof(TransactionId) * orig_snap->xcnt +
 		sizeof(TransactionId) * (txn->nsubtxns + 1);
 
 	snap = MemoryContextAllocZero(rb->context, size);
-	memcpy(snap, orig_snap, sizeof(SnapshotData));
+	memcpy(snap, orig_snap, sizeof(HistoricMVCCSnapshotData));
 
 	snap->copied = true;
-	snap->active_count = 1;		/* mark as active so nobody frees it */
+	snap->refcount = 1;			/* mark as active so nobody frees it */
+	snap->active_count = 0;
 	snap->regd_count = 0;
-	snap->xip = (TransactionId *) (snap + 1);
+	snap->committed_xids = (TransactionId *) (snap + 1);
 
-	memcpy(snap->xip, orig_snap->xip, sizeof(TransactionId) * snap->xcnt);
+	memcpy(snap->committed_xids, orig_snap->committed_xids, sizeof(TransactionId) * snap->xcnt);
 
 	/*
-	 * snap->subxip contains all txids that belong to our transaction which we
+	 * snap->curxip contains all txids that belong to our transaction which we
 	 * need to check via cmin/cmax. That's why we store the toplevel
 	 * transaction in there as well.
 	 */
-	snap->subxip = snap->xip + snap->xcnt;
-	snap->subxip[i++] = txn->xid;
+	snap->curxip = snap->committed_xids + snap->xcnt;
+	snap->curxip[i++] = txn->xid;
 
 	/*
 	 * txn->nsubtxns isn't decreased when subtransactions abort, so count
 	 * manually. Since it's an upper boundary it is safe to use it for the
 	 * allocation above.
 	 */
-	snap->subxcnt = 1;
+	snap->curxcnt = 1;
 
 	dlist_foreach(iter, &txn->subtxns)
 	{
 		ReorderBufferTXN *sub_txn;
 
 		sub_txn = dlist_container(ReorderBufferTXN, node, iter.cur);
-		snap->subxip[i++] = sub_txn->xid;
-		snap->subxcnt++;
+		snap->curxip[i++] = sub_txn->xid;
+		snap->curxcnt++;
 	}
 
 	/* sort so we can bsearch() later */
-	qsort(snap->subxip, snap->subxcnt, sizeof(TransactionId), xidComparator);
+	qsort(snap->curxip, snap->curxcnt, sizeof(TransactionId), xidComparator);
 
 	/* store the specified current CommandId */
 	snap->curcid = cid;
@@ -1965,7 +1966,7 @@ ReorderBufferCopySnap(ReorderBuffer *rb, Snapshot orig_snap,
  * Free a previously ReorderBufferCopySnap'ed snapshot
  */
 static void
-ReorderBufferFreeSnap(ReorderBuffer *rb, Snapshot snap)
+ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap)
 {
 	if (snap->copied)
 		pfree(snap);
@@ -2118,7 +2119,7 @@ ReorderBufferApplyMessage(ReorderBuffer *rb, ReorderBufferTXN *txn,
  */
 static inline void
 ReorderBufferSaveTXNSnapshot(ReorderBuffer *rb, ReorderBufferTXN *txn,
-							 Snapshot snapshot_now, CommandId command_id)
+							 HistoricMVCCSnapshot snapshot_now, CommandId command_id)
 {
 	txn->command_id = command_id;
 
@@ -2163,7 +2164,7 @@ ReorderBufferMaybeMarkTXNStreamed(ReorderBuffer *rb, ReorderBufferTXN *txn)
  */
 static void
 ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
-					  Snapshot snapshot_now,
+					  HistoricMVCCSnapshot snapshot_now,
 					  CommandId command_id,
 					  XLogRecPtr last_lsn,
 					  ReorderBufferChange *specinsert)
@@ -2210,7 +2211,7 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 static void
 ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 						XLogRecPtr commit_lsn,
-						volatile Snapshot snapshot_now,
+						volatile HistoricMVCCSnapshot snapshot_now,
 						volatile CommandId command_id,
 						bool streaming)
 {
@@ -2826,7 +2827,7 @@ ReorderBufferReplay(ReorderBufferTXN *txn,
 					TimestampTz commit_time,
 					RepOriginId origin_id, XLogRecPtr origin_lsn)
 {
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id = FirstCommandId;
 
 	txn->final_lsn = commit_lsn;
@@ -3306,7 +3307,7 @@ ReorderBufferProcessXid(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn)
  */
 void
 ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
-						 XLogRecPtr lsn, Snapshot snap)
+						 XLogRecPtr lsn, HistoricMVCCSnapshot snap)
 {
 	ReorderBufferChange *change = ReorderBufferAllocChange(rb);
 
@@ -3324,7 +3325,7 @@ ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
  */
 void
 ReorderBufferSetBaseSnapshot(ReorderBuffer *rb, TransactionId xid,
-							 XLogRecPtr lsn, Snapshot snap)
+							 XLogRecPtr lsn, HistoricMVCCSnapshot snap)
 {
 	ReorderBufferTXN *txn;
 	bool		is_new;
@@ -4207,14 +4208,14 @@ ReorderBufferSerializeChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	snap;
+				HistoricMVCCSnapshot snap;
 				char	   *data;
 
 				snap = change->data.snapshot;
 
-				sz += sizeof(SnapshotData) +
+				sz += sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * snap->xcnt +
-					sizeof(TransactionId) * snap->subxcnt;
+					sizeof(TransactionId) * snap->curxcnt;
 
 				/* make sure we have enough space */
 				ReorderBufferSerializeReserve(rb, sz);
@@ -4222,21 +4223,21 @@ ReorderBufferSerializeChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				/* might have been reallocated above */
 				ondisk = (ReorderBufferDiskChange *) rb->outbuf;
 
-				memcpy(data, snap, sizeof(SnapshotData));
-				data += sizeof(SnapshotData);
+				memcpy(data, snap, sizeof(HistoricMVCCSnapshotData));
+				data += sizeof(HistoricMVCCSnapshotData);
 
 				if (snap->xcnt)
 				{
-					memcpy(data, snap->xip,
+					memcpy(data, snap->committed_xids,
 						   sizeof(TransactionId) * snap->xcnt);
 					data += sizeof(TransactionId) * snap->xcnt;
 				}
 
-				if (snap->subxcnt)
+				if (snap->curxcnt)
 				{
-					memcpy(data, snap->subxip,
-						   sizeof(TransactionId) * snap->subxcnt);
-					data += sizeof(TransactionId) * snap->subxcnt;
+					memcpy(data, snap->curxip,
+						   sizeof(TransactionId) * snap->curxcnt);
+					data += sizeof(TransactionId) * snap->curxcnt;
 				}
 				break;
 			}
@@ -4341,7 +4342,7 @@ ReorderBufferCanStartStreaming(ReorderBuffer *rb)
 static void
 ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 {
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id;
 	Size		stream_bytes;
 	bool		txn_is_streamed;
@@ -4360,10 +4361,10 @@ ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	 * After that we need to reuse the snapshot from the previous run.
 	 *
 	 * Unlike DecodeCommit which adds xids of all the subtransactions in
-	 * snapshot's xip array via SnapBuildCommitTxn, we can't do that here but
-	 * we do add them to subxip array instead via ReorderBufferCopySnap. This
-	 * allows the catalog changes made in subtransactions decoded till now to
-	 * be visible.
+	 * snapshot's committed_xids array via SnapBuildCommitTxn, we can't do
+	 * that here but we do add them to curxip array instead via
+	 * ReorderBufferCopySnap. This allows the catalog changes made in
+	 * subtransactions decoded till now to be visible.
 	 */
 	if (txn->snapshot_now == NULL)
 	{
@@ -4509,13 +4510,13 @@ ReorderBufferChangeSize(ReorderBufferChange *change)
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	snap;
+				HistoricMVCCSnapshot snap;
 
 				snap = change->data.snapshot;
 
-				sz += sizeof(SnapshotData) +
+				sz += sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * snap->xcnt +
-					sizeof(TransactionId) * snap->subxcnt;
+					sizeof(TransactionId) * snap->curxcnt;
 
 				break;
 			}
@@ -4793,24 +4794,24 @@ ReorderBufferRestoreChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			}
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			{
-				Snapshot	oldsnap;
-				Snapshot	newsnap;
+				HistoricMVCCSnapshot oldsnap;
+				HistoricMVCCSnapshot newsnap;
 				Size		size;
 
-				oldsnap = (Snapshot) data;
+				oldsnap = (HistoricMVCCSnapshot) data;
 
-				size = sizeof(SnapshotData) +
+				size = sizeof(HistoricMVCCSnapshotData) +
 					sizeof(TransactionId) * oldsnap->xcnt +
-					sizeof(TransactionId) * (oldsnap->subxcnt + 0);
+					sizeof(TransactionId) * (oldsnap->curxcnt + 0);
 
 				change->data.snapshot = MemoryContextAllocZero(rb->context, size);
 
 				newsnap = change->data.snapshot;
 
 				memcpy(newsnap, data, size);
-				newsnap->xip = (TransactionId *)
-					(((char *) newsnap) + sizeof(SnapshotData));
-				newsnap->subxip = newsnap->xip + newsnap->xcnt;
+				newsnap->committed_xids = (TransactionId *)
+					(((char *) newsnap) + sizeof(HistoricMVCCSnapshotData));
+				newsnap->curxip = newsnap->committed_xids + newsnap->xcnt;
 				newsnap->copied = true;
 				break;
 			}
@@ -5476,7 +5477,7 @@ file_sort_by_lsn(const ListCell *a_p, const ListCell *b_p)
  * transaction for relid.
  */
 static void
-UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
+UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, HistoricMVCCSnapshot snapshot)
 {
 	DIR		   *mapping_dir;
 	struct dirent *mapping_de;
@@ -5524,7 +5525,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
 			continue;
 
 		/* not for our transaction */
-		if (!TransactionIdInArray(f_mapped_xid, snapshot->subxip, snapshot->subxcnt))
+		if (!TransactionIdInArray(f_mapped_xid, snapshot->curxip, snapshot->curxcnt))
 			continue;
 
 		/* ok, relevant, queue for apply */
@@ -5543,7 +5544,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
 		RewriteMappingFile *f = (RewriteMappingFile *) lfirst(file);
 
 		elog(DEBUG1, "applying mapping: \"%s\" in %u", f->fname,
-			 snapshot->subxip[0]);
+			 snapshot->curxip[0]);
 		ApplyLogicalMappingFile(tuplecid_data, relid, f->fname);
 		pfree(f);
 	}
@@ -5555,7 +5556,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
  */
 bool
 ResolveCminCmaxDuringDecoding(HTAB *tuplecid_data,
-							  Snapshot snapshot,
+							  HistoricMVCCSnapshot snapshot,
 							  HeapTuple htup, Buffer buffer,
 							  CommandId *cmin, CommandId *cmax)
 {
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index d6ab1e017eb..a4d2288961b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -155,11 +155,11 @@ static bool ExportInProgress = false;
 static void SnapBuildPurgeOlderTxn(SnapBuild *builder);
 
 /* snapshot building/manipulation/distribution functions */
-static Snapshot SnapBuildBuildSnapshot(SnapBuild *builder);
+static HistoricMVCCSnapshot SnapBuildBuildSnapshot(SnapBuild *builder);
 
-static void SnapBuildFreeSnapshot(Snapshot snap);
+static void SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap);
 
-static void SnapBuildSnapIncRefcount(Snapshot snap);
+static void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
 
 static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
 
@@ -249,15 +249,13 @@ FreeSnapshotBuilder(SnapBuild *builder)
  * Free an unreferenced snapshot that has previously been built by us.
  */
 static void
-SnapBuildFreeSnapshot(Snapshot snap)
+SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap)
 {
 	/* make sure we don't get passed an external snapshot */
-	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
+	Assert(snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
 	/* make sure nobody modified our snapshot */
 	Assert(snap->curcid == FirstCommandId);
-	Assert(!snap->suboverflowed);
-	Assert(!snap->takenDuringRecovery);
 	Assert(snap->regd_count == 0);
 
 	/* slightly more likely, so it's checked even without c-asserts */
@@ -266,6 +264,8 @@ SnapBuildFreeSnapshot(Snapshot snap)
 
 	if (snap->active_count)
 		elog(ERROR, "cannot free an active snapshot");
+	if (snap->refcount)
+		elog(ERROR, "cannot free a snapshot that's in use");
 
 	pfree(snap);
 }
@@ -313,9 +313,9 @@ SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr)
  * adding a Snapshot as builder->snapshot.
  */
 static void
-SnapBuildSnapIncRefcount(Snapshot snap)
+SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 {
-	snap->active_count++;
+	snap->refcount++;
 }
 
 /*
@@ -325,26 +325,25 @@ SnapBuildSnapIncRefcount(Snapshot snap)
  * IncRef'ed Snapshot can adjust its refcount easily.
  */
 void
-SnapBuildSnapDecRefcount(Snapshot snap)
+SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
 {
 	/* make sure we don't get passed an external snapshot */
-	Assert(snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);
+	Assert(snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
 	/* make sure nobody modified our snapshot */
 	Assert(snap->curcid == FirstCommandId);
-	Assert(!snap->suboverflowed);
-	Assert(!snap->takenDuringRecovery);
 
+	Assert(snap->refcount > 0);
 	Assert(snap->regd_count == 0);
 
-	Assert(snap->active_count > 0);
-
 	/* slightly more likely, so it's checked even without casserts */
 	if (snap->copied)
 		elog(ERROR, "cannot free a copied snapshot");
+	if (snap->active_count)
+		elog(ERROR, "cannot free an active snapshot");
 
-	snap->active_count--;
-	if (snap->active_count == 0)
+	snap->refcount--;
+	if (snap->refcount == 0)
 		SnapBuildFreeSnapshot(snap);
 }
 
@@ -356,21 +355,21 @@ SnapBuildSnapDecRefcount(Snapshot snap)
  * these snapshots; they have to copy them and fill in appropriate ->curcid
  * and ->subxip/subxcnt values.
  */
-static Snapshot
+static HistoricMVCCSnapshot
 SnapBuildBuildSnapshot(SnapBuild *builder)
 {
-	Snapshot	snapshot;
+	HistoricMVCCSnapshot snapshot;
 	Size		ssize;
 
 	Assert(builder->state >= SNAPBUILD_FULL_SNAPSHOT);
 
-	ssize = sizeof(SnapshotData)
+	ssize = sizeof(HistoricMVCCSnapshotData)
 		+ sizeof(TransactionId) * builder->committed.xcnt
 		+ sizeof(TransactionId) * 1 /* toplevel xid */ ;
 
 	snapshot = MemoryContextAllocZero(builder->context, ssize);
 
-	snapshot->snapshot_type = SNAPSHOT_HISTORIC_MVCC;
+	snapshot->base.snapshot_type = SNAPSHOT_HISTORIC_MVCC;
 
 	/*
 	 * We misuse the original meaning of SnapshotData's xip and subxip fields
@@ -400,31 +399,29 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
 	snapshot->xmax = builder->xmax;
 
 	/* store all transactions to be treated as committed by this snapshot */
-	snapshot->xip =
-		(TransactionId *) ((char *) snapshot + sizeof(SnapshotData));
+	snapshot->committed_xids =
+		(TransactionId *) ((char *) snapshot + sizeof(HistoricMVCCSnapshotData));
 	snapshot->xcnt = builder->committed.xcnt;
-	memcpy(snapshot->xip,
+	memcpy(snapshot->committed_xids,
 		   builder->committed.xip,
 		   builder->committed.xcnt * sizeof(TransactionId));
 
 	/* sort so we can bsearch() */
-	qsort(snapshot->xip, snapshot->xcnt, sizeof(TransactionId), xidComparator);
+	qsort(snapshot->committed_xids, snapshot->xcnt, sizeof(TransactionId), xidComparator);
 
 	/*
-	 * Initially, subxip is empty, i.e. it's a snapshot to be used by
+	 * Initially, curxip is empty, i.e. it's a snapshot to be used by
 	 * transactions that don't modify the catalog. Will be filled by
 	 * ReorderBufferCopySnap() if necessary.
 	 */
-	snapshot->subxcnt = 0;
-	snapshot->subxip = NULL;
+	snapshot->curxcnt = 0;
+	snapshot->curxip = NULL;
 
-	snapshot->suboverflowed = false;
-	snapshot->takenDuringRecovery = false;
 	snapshot->copied = false;
 	snapshot->curcid = FirstCommandId;
+	snapshot->refcount = 0;
 	snapshot->active_count = 0;
 	snapshot->regd_count = 0;
-	snapshot->snapXactCompletionCount = 0;
 
 	return snapshot;
 }
@@ -436,13 +433,13 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
  * The snapshot will be usable directly in current transaction or exported
  * for loading in different transaction.
  */
-Snapshot
+MVCCSnapshot
 SnapBuildInitialSnapshot(SnapBuild *builder)
 {
-	Snapshot	snap;
+	HistoricMVCCSnapshot historicsnap;
+	MVCCSnapshot mvccsnap;
 	TransactionId xid;
 	TransactionId safeXid;
-	TransactionId *newxip;
 	int			newxcnt = 0;
 
 	Assert(XactIsoLevel == XACT_REPEATABLE_READ);
@@ -464,10 +461,10 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	if (TransactionIdIsValid(MyProc->xmin))
 		elog(ERROR, "cannot build an initial slot snapshot when MyProc->xmin already is valid");
 
-	snap = SnapBuildBuildSnapshot(builder);
+	historicsnap = SnapBuildBuildSnapshot(builder);
 
 	/*
-	 * We know that snap->xmin is alive, enforced by the logical xmin
+	 * We know that historicsnap->xmin is alive, enforced by the logical xmin
 	 * mechanism. Due to that we can do this without locks, we're only
 	 * changing our own value.
 	 *
@@ -479,14 +476,18 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	safeXid = GetOldestSafeDecodingTransactionId(false);
 	LWLockRelease(ProcArrayLock);
 
-	if (TransactionIdFollows(safeXid, snap->xmin))
+	if (TransactionIdFollows(safeXid, historicsnap->xmin))
 		elog(ERROR, "cannot build an initial slot snapshot as oldest safe xid %u follows snapshot's xmin %u",
-			 safeXid, snap->xmin);
+			 safeXid, historicsnap->xmin);
 
-	MyProc->xmin = snap->xmin;
+	MyProc->xmin = historicsnap->xmin;
 
 	/* allocate in transaction context */
-	newxip = palloc_array(TransactionId, GetMaxSnapshotXidCount());
+	mvccsnap = palloc(sizeof(MVCCSnapshotData) + sizeof(TransactionId) * GetMaxSnapshotXidCount());
+	mvccsnap->base.snapshot_type = SNAPSHOT_MVCC;
+	mvccsnap->xmin = historicsnap->xmin;
+	mvccsnap->xmax = historicsnap->xmax;
+	mvccsnap->xip = (TransactionId *) ((char *) mvccsnap + sizeof(MVCCSnapshotData));
 
 	/*
 	 * snapbuild.c builds transactions in an "inverted" manner, which means it
@@ -494,15 +495,15 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	 * classical snapshot by marking all non-committed transactions as
 	 * in-progress. This can be expensive.
 	 */
-	for (xid = snap->xmin; NormalTransactionIdPrecedes(xid, snap->xmax);)
+	for (xid = historicsnap->xmin; NormalTransactionIdPrecedes(xid, historicsnap->xmax);)
 	{
 		void	   *test;
 
 		/*
-		 * Check whether transaction committed using the decoding snapshot
-		 * meaning of ->xip.
+		 * Check whether transaction committed using the decoding snapshot's
+		 * committed_xids array.
 		 */
-		test = bsearch(&xid, snap->xip, snap->xcnt,
+		test = bsearch(&xid, historicsnap->committed_xids, historicsnap->xcnt,
 					   sizeof(TransactionId), xidComparator);
 
 		if (test == NULL)
@@ -512,18 +513,27 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
 						 errmsg("initial slot snapshot too large")));
 
-			newxip[newxcnt++] = xid;
+			mvccsnap->xip[newxcnt++] = xid;
 		}
 
 		TransactionIdAdvance(xid);
 	}
-
-	/* adjust remaining snapshot fields as needed */
-	snap->snapshot_type = SNAPSHOT_MVCC;
-	snap->xcnt = newxcnt;
-	snap->xip = newxip;
-
-	return snap;
+	mvccsnap->xcnt = newxcnt;
+
+	/* Initialize remaining MVCCSnapshot fields */
+	mvccsnap->subxip = NULL;
+	mvccsnap->subxcnt = 0;
+	mvccsnap->suboverflowed = false;
+	mvccsnap->takenDuringRecovery = false;
+	mvccsnap->copied = true;
+	mvccsnap->curcid = FirstCommandId;
+	mvccsnap->active_count = 0;
+	mvccsnap->regd_count = 0;
+	mvccsnap->snapXactCompletionCount = 0;
+
+	pfree(historicsnap);
+
+	return mvccsnap;
 }
 
 /*
@@ -537,7 +547,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 const char *
 SnapBuildExportSnapshot(SnapBuild *builder)
 {
-	Snapshot	snap;
+	MVCCSnapshot snap;
 	char	   *snapname;
 
 	if (IsTransactionOrTransactionBlock())
@@ -574,7 +584,7 @@ SnapBuildExportSnapshot(SnapBuild *builder)
 /*
  * Ensure there is a snapshot and if not build one for current transaction.
  */
-Snapshot
+HistoricMVCCSnapshot
 SnapBuildGetOrBuildSnapshot(SnapBuild *builder)
 {
 	Assert(builder->state == SNAPBUILD_CONSISTENT);
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 449632ad1aa..ac8a9d33323 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1329,7 +1329,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		}
 		else if (snapshot_action == CRS_USE_SNAPSHOT)
 		{
-			Snapshot	snap;
+			MVCCSnapshot snap;
 
 			snap = SnapBuildInitialSnapshot(ctx->snapshot_builder);
 			RestoreTransactionSnapshot(snap, MyProc);
diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index f3a1603204e..2292d8a09af 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2040,7 +2040,7 @@ GetMaxSnapshotSubxidCount(void)
  * least in the case we already hold a snapshot), but that's for another day.
  */
 static bool
-GetSnapshotDataReuse(Snapshot snapshot)
+GetSnapshotDataReuse(MVCCSnapshot snapshot)
 {
 	uint64		curXactCompletionCount;
 
@@ -2119,8 +2119,8 @@ GetSnapshotDataReuse(Snapshot snapshot)
  * Note: this function should probably not be called with an argument that's
  * not statically allocated (see xip allocation below).
  */
-Snapshot
-GetSnapshotData(Snapshot snapshot)
+MVCCSnapshot
+GetSnapshotData(MVCCSnapshot snapshot)
 {
 	ProcArrayStruct *arrayP = procArray;
 	TransactionId *other_xids = ProcGlobal->xids;
diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index 149c69a1873..8d97127668c 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -449,10 +449,10 @@ static void SerialSetActiveSerXmin(TransactionId xid);
 
 static uint32 predicatelock_hash(const void *key, Size keysize);
 static void SummarizeOldestCommittedSxact(void);
-static Snapshot GetSafeSnapshot(Snapshot origSnapshot);
-static Snapshot GetSerializableTransactionSnapshotInt(Snapshot snapshot,
-													  VirtualTransactionId *sourcevxid,
-													  int sourcepid);
+static MVCCSnapshot GetSafeSnapshot(MVCCSnapshot origSnapshot);
+static MVCCSnapshot GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
+														  VirtualTransactionId *sourcevxid,
+														  int sourcepid);
 static bool PredicateLockExists(const PREDICATELOCKTARGETTAG *targettag);
 static bool GetParentPredicateLockTag(const PREDICATELOCKTARGETTAG *tag,
 									  PREDICATELOCKTARGETTAG *parent);
@@ -1552,10 +1552,10 @@ SummarizeOldestCommittedSxact(void)
  *		for), the passed-in Snapshot pointer should reference a static data
  *		area that can safely be passed to GetSnapshotData.
  */
-static Snapshot
-GetSafeSnapshot(Snapshot origSnapshot)
+static MVCCSnapshot
+GetSafeSnapshot(MVCCSnapshot origSnapshot)
 {
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 
 	Assert(XactReadOnly && XactDeferrable);
 
@@ -1676,8 +1676,8 @@ GetSafeSnapshotBlockingPids(int blocked_pid, int *output, int output_size)
  * always this same pointer; no new snapshot data structure is allocated
  * within this function.
  */
-Snapshot
-GetSerializableTransactionSnapshot(Snapshot snapshot)
+MVCCSnapshot
+GetSerializableTransactionSnapshot(MVCCSnapshot snapshot)
 {
 	Assert(IsolationIsSerializable());
 
@@ -1717,7 +1717,7 @@ GetSerializableTransactionSnapshot(Snapshot snapshot)
  * read-only.
  */
 void
-SetSerializableTransactionSnapshot(Snapshot snapshot,
+SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
 								   VirtualTransactionId *sourcevxid,
 								   int sourcepid)
 {
@@ -1758,8 +1758,8 @@ SetSerializableTransactionSnapshot(Snapshot snapshot,
  * source xact is still running after we acquire SerializableXactHashLock.
  * We do that by calling ProcArrayInstallImportedXmin.
  */
-static Snapshot
-GetSerializableTransactionSnapshotInt(Snapshot snapshot,
+static MVCCSnapshot
+GetSerializableTransactionSnapshotInt(MVCCSnapshot snapshot,
 									  VirtualTransactionId *sourcevxid,
 									  int sourcepid)
 {
@@ -3969,12 +3969,12 @@ ReleaseOneSerializableXact(SERIALIZABLEXACT *sxact, bool partial,
 static bool
 XidIsConcurrent(TransactionId xid)
 {
-	Snapshot	snap;
+	MVCCSnapshot snap;
 
 	Assert(TransactionIdIsValid(xid));
 	Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()));
 
-	snap = GetTransactionSnapshot();
+	snap = (MVCCSnapshot) GetTransactionSnapshot();
 
 	if (TransactionIdPrecedes(xid, snap->xmin))
 		return false;
@@ -4222,7 +4222,7 @@ CheckTargetForConflictsIn(PREDICATELOCKTARGETTAG *targettag)
 		}
 		else if (!SxactIsDoomed(sxact)
 				 && (!SxactIsCommitted(sxact)
-					 || TransactionIdPrecedes(GetTransactionSnapshot()->xmin,
+					 || TransactionIdPrecedes(TransactionXmin,
 											  sxact->finishedBefore))
 				 && !RWConflictExists(sxact, MySerializableXact))
 		{
@@ -4235,7 +4235,7 @@ CheckTargetForConflictsIn(PREDICATELOCKTARGETTAG *targettag)
 			 */
 			if (!SxactIsDoomed(sxact)
 				&& (!SxactIsCommitted(sxact)
-					|| TransactionIdPrecedes(GetTransactionSnapshot()->xmin,
+					|| TransactionIdPrecedes(TransactionXmin,
 											 sxact->finishedBefore))
 				&& !RWConflictExists(sxact, MySerializableXact))
 			{
diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index 7b38eec6345..2880c180f78 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -373,14 +373,16 @@ pg_current_snapshot(PG_FUNCTION_ARGS)
 	pg_snapshot *snap;
 	uint32		nxip,
 				i;
-	Snapshot	cur;
+	Snapshot	active_snap;
+	MVCCSnapshot cur;
 	FullTransactionId next_fxid = ReadNextFullTransactionId();
 
-	cur = GetActiveSnapshot();
-	if (cur == NULL)
+	active_snap = GetActiveSnapshot();
+	if (active_snap == NULL)
 		elog(ERROR, "no active snapshot set");
-	if (cur->snapshot_type != SNAPSHOT_MVCC)
+	if (active_snap->base.snapshot_type != SNAPSHOT_MVCC)
 		elog(ERROR, "pg_current_snapshot() cannot be used with a non-MVCC snapshot");
+	cur = &active_snap->mvcc;
 
 	/* allocate */
 	nxip = cur->xcnt;
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index f156845231b..f16f3fd48e5 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -138,18 +138,18 @@
  * These SnapshotData structs are static to simplify memory allocation
  * (see the hack in GetSnapshotData to avoid repeated malloc/free).
  */
-static SnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
-static SnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
-static SnapshotData CatalogSnapshotData = {SNAPSHOT_MVCC};
-SnapshotData SnapshotSelfData = {SNAPSHOT_SELF};
-SnapshotData SnapshotAnyData = {SNAPSHOT_ANY};
-SnapshotData SnapshotToastData = {SNAPSHOT_TOAST};
+static MVCCSnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
+static MVCCSnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
+static MVCCSnapshotData CatalogSnapshotData = {SNAPSHOT_MVCC};
+SnapshotBaseData SnapshotSelfData = {SNAPSHOT_SELF};
+SnapshotBaseData SnapshotAnyData = {SNAPSHOT_ANY};
+SnapshotBaseData SnapshotToastData = {SNAPSHOT_TOAST};
 
 /* Pointers to valid snapshots */
-static Snapshot CurrentSnapshot = NULL;
-static Snapshot SecondarySnapshot = NULL;
-static Snapshot CatalogSnapshot = NULL;
-static Snapshot HistoricSnapshot = NULL;
+static MVCCSnapshot CurrentSnapshot = NULL;
+static MVCCSnapshot SecondarySnapshot = NULL;
+static MVCCSnapshot CatalogSnapshot = NULL;
+static HistoricMVCCSnapshot HistoricSnapshot = NULL;
 
 /*
  * These are updated by GetSnapshotData.  We initialize them this way
@@ -197,7 +197,7 @@ bool		FirstSnapshotSet = false;
  * FirstSnapshotSet in combination with IsolationUsesXactSnapshot(), because
  * GUC may be reset before us, changing the value of IsolationUsesXactSnapshot.
  */
-static Snapshot FirstXactSnapshot = NULL;
+static MVCCSnapshot FirstXactSnapshot = NULL;
 
 /* Define pathname of exported-snapshot files */
 #define SNAPSHOT_EXPORT_DIR "pg_snapshots"
@@ -206,16 +206,16 @@ static Snapshot FirstXactSnapshot = NULL;
 typedef struct ExportedSnapshot
 {
 	char	   *snapfile;
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 } ExportedSnapshot;
 
 /* Current xact's exported snapshots (a list of ExportedSnapshot structs) */
 static List *exportedSnapshots = NIL;
 
 /* Prototypes for local functions */
-static Snapshot CopySnapshot(Snapshot snapshot);
+static MVCCSnapshot CopyMVCCSnapshot(MVCCSnapshot snapshot);
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
-static void FreeSnapshot(Snapshot snapshot);
+static void FreeMVCCSnapshot(MVCCSnapshot snapshot);
 static void SnapshotResetXmin(void);
 
 /* ResourceOwner callbacks to track snapshot references */
@@ -287,7 +287,7 @@ GetTransactionSnapshot(void)
 		 * for later calls to GetTransactionSnapshot().
 		 */
 		Assert(!FirstSnapshotSet);
-		return HistoricSnapshot;
+		return (Snapshot) HistoricSnapshot;
 	}
 
 	/* First call in transaction? */
@@ -320,8 +320,9 @@ GetTransactionSnapshot(void)
 				CurrentSnapshot = GetSerializableTransactionSnapshot(&CurrentSnapshotData);
 			else
 				CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+
 			/* Make a saved copy */
-			CurrentSnapshot = CopySnapshot(CurrentSnapshot);
+			CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
 			FirstXactSnapshot = CurrentSnapshot;
 			/* Mark it as "registered" in FirstXactSnapshot */
 			FirstXactSnapshot->regd_count++;
@@ -331,18 +332,18 @@ GetTransactionSnapshot(void)
 			CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
 
 		FirstSnapshotSet = true;
-		return CurrentSnapshot;
+		return (Snapshot) CurrentSnapshot;
 	}
 
 	if (IsolationUsesXactSnapshot())
-		return CurrentSnapshot;
+		return (Snapshot) CurrentSnapshot;
 
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
 	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
 
-	return CurrentSnapshot;
+	return (Snapshot) CurrentSnapshot;
 }
 
 /*
@@ -373,7 +374,7 @@ GetLatestSnapshot(void)
 
 	SecondarySnapshot = GetSnapshotData(&SecondarySnapshotData);
 
-	return SecondarySnapshot;
+	return (Snapshot) SecondarySnapshot;
 }
 
 /*
@@ -392,7 +393,7 @@ GetCatalogSnapshot(Oid relid)
 	 * finishing decoding.
 	 */
 	if (HistoricSnapshotActive())
-		return HistoricSnapshot;
+		return (Snapshot) HistoricSnapshot;
 
 	return GetNonHistoricCatalogSnapshot(relid);
 }
@@ -438,7 +439,7 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
 	}
 
-	return CatalogSnapshot;
+	return (Snapshot) CatalogSnapshot;
 }
 
 /*
@@ -508,7 +509,7 @@ SnapshotSetCommandId(CommandId curcid)
  * in GetTransactionSnapshot.
  */
 static void
-SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
+SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid,
 					   int sourcepid, PGPROC *sourceproc)
 {
 	/* Caller should have checked this already */
@@ -587,7 +588,7 @@ SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
 			SetSerializableTransactionSnapshot(CurrentSnapshot, sourcevxid,
 											   sourcepid);
 		/* Make a saved copy */
-		CurrentSnapshot = CopySnapshot(CurrentSnapshot);
+		CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
 		FirstXactSnapshot = CurrentSnapshot;
 		/* Mark it as "registered" in FirstXactSnapshot */
 		FirstXactSnapshot->regd_count++;
@@ -598,29 +599,27 @@ SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid,
 }
 
 /*
- * CopySnapshot
+ * CopyMVCCSnapshot
  *		Copy the given snapshot.
  *
  * The copy is palloc'd in TopTransactionContext and has initial refcounts set
  * to 0.  The returned snapshot has the copied flag set.
  */
-static Snapshot
-CopySnapshot(Snapshot snapshot)
+static MVCCSnapshot
+CopyMVCCSnapshot(MVCCSnapshot snapshot)
 {
-	Snapshot	newsnap;
+	MVCCSnapshot newsnap;
 	Size		subxipoff;
 	Size		size;
 
-	Assert(snapshot != InvalidSnapshot);
-
 	/* We allocate any XID arrays needed in the same palloc block. */
-	size = subxipoff = sizeof(SnapshotData) +
+	size = subxipoff = sizeof(MVCCSnapshotData) +
 		snapshot->xcnt * sizeof(TransactionId);
 	if (snapshot->subxcnt > 0)
 		size += snapshot->subxcnt * sizeof(TransactionId);
 
-	newsnap = (Snapshot) MemoryContextAlloc(TopTransactionContext, size);
-	memcpy(newsnap, snapshot, sizeof(SnapshotData));
+	newsnap = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
+	memcpy(newsnap, snapshot, sizeof(MVCCSnapshotData));
 
 	newsnap->regd_count = 0;
 	newsnap->active_count = 0;
@@ -657,11 +656,11 @@ CopySnapshot(Snapshot snapshot)
 }
 
 /*
- * FreeSnapshot
+ * FreeMVCCSnapshot
  *		Free the memory associated with a snapshot.
  */
 static void
-FreeSnapshot(Snapshot snapshot)
+FreeMVCCSnapshot(MVCCSnapshot snapshot)
 {
 	Assert(snapshot->regd_count == 0);
 	Assert(snapshot->active_count == 0);
@@ -677,6 +676,11 @@ FreeSnapshot(Snapshot snapshot)
  * If the passed snapshot is a statically-allocated one, or it is possibly
  * subject to a future command counter update, create a new long-lived copy
  * with active refcount=1.  Otherwise, only increment the refcount.
+ *
+ * Only a regular MVCC snaphot or a historical MVCC snapshot can be used as
+ * the active snapshot.  Core code only ever uses a regular MVCC snapshot, but
+ * extensions may use historic MVCC snapshots too (pglogical is a known
+ * example).
  */
 void
 PushActiveSnapshot(Snapshot snapshot)
@@ -697,26 +701,41 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 {
 	ActiveSnapshotElt *newactive;
 
-	Assert(snapshot != InvalidSnapshot);
 	Assert(ActiveSnapshot == NULL || snap_level >= ActiveSnapshot->as_level);
 
 	newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
 
-	/*
-	 * Checking SecondarySnapshot is probably useless here, but it seems
-	 * better to be sure.
-	 */
-	if (snapshot == CurrentSnapshot || snapshot == SecondarySnapshot ||
-		!snapshot->copied)
-		newactive->as_snap = CopySnapshot(snapshot);
+	if (snapshot->base.snapshot_type == SNAPSHOT_MVCC)
+	{
+		MVCCSnapshot origsnap = &snapshot->mvcc;
+		MVCCSnapshot newsnap;
+
+		/*
+		 * Checking SecondarySnapshot is probably useless here, but it seems
+		 * better to be sure.
+		 */
+		if (origsnap == CurrentSnapshot || origsnap == SecondarySnapshot ||
+			!origsnap->copied)
+			newsnap = CopyMVCCSnapshot(origsnap);
+		else
+			newsnap = origsnap;
+		newsnap->active_count++;
+		newactive->as_snap = (Snapshot) newsnap;
+	}
+	else if (snapshot->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	{
+		HistoricMVCCSnapshot snap = &snapshot->historic_mvcc;
+
+		snap->active_count++;
+		newactive->as_snap = (Snapshot) snap;
+		elog(LOG, "pushed historic snap %p, count %d", snap, snap->active_count);
+	}
 	else
-		newactive->as_snap = snapshot;
+		elog(ERROR, "cannot push snapshot of kind %d", snapshot->base.snapshot_type);
 
 	newactive->as_next = ActiveSnapshot;
 	newactive->as_level = snap_level;
 
-	newactive->as_snap->active_count++;
-
 	ActiveSnapshot = newactive;
 }
 
@@ -731,7 +750,8 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 void
 PushCopiedSnapshot(Snapshot snapshot)
 {
-	PushActiveSnapshot(CopySnapshot(snapshot));
+	Assert(snapshot->base.snapshot_type == SNAPSHOT_MVCC);
+	PushActiveSnapshot((Snapshot) CopyMVCCSnapshot(&snapshot->mvcc));
 }
 
 /*
@@ -743,26 +763,43 @@ PushCopiedSnapshot(Snapshot snapshot)
 void
 UpdateActiveSnapshotCommandId(void)
 {
-	CommandId	save_curcid,
-				curcid;
+	CommandId	curcid;
 
 	Assert(ActiveSnapshot != NULL);
-	Assert(ActiveSnapshot->as_snap->active_count == 1);
-	Assert(ActiveSnapshot->as_snap->regd_count == 0);
 
-	/*
-	 * Don't allow modification of the active snapshot during parallel
-	 * operation.  We share the snapshot to worker backends at the beginning
-	 * of parallel operation, so any change to the snapshot can lead to
-	 * inconsistencies.  We have other defenses against
-	 * CommandCounterIncrement, but there are a few places that call this
-	 * directly, so we put an additional guard here.
-	 */
-	save_curcid = ActiveSnapshot->as_snap->curcid;
 	curcid = GetCurrentCommandId(false);
-	if (IsInParallelMode() && save_curcid != curcid)
-		elog(ERROR, "cannot modify commandid in active snapshot during a parallel operation");
-	ActiveSnapshot->as_snap->curcid = curcid;
+
+	if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_MVCC)
+	{
+		MVCCSnapshot activesnap = &ActiveSnapshot->as_snap->mvcc;
+
+		Assert(activesnap->active_count == 1);
+		Assert(activesnap->regd_count == 0);
+
+		/*
+		 * Don't allow modification of the active snapshot during parallel
+		 * operation.  We share the snapshot to worker backends at the
+		 * beginning of parallel operation, so any change to the snapshot can
+		 * lead to inconsistencies.  We have other defenses against
+		 * CommandCounterIncrement, but there are a few places that call this
+		 * directly, so we put an additional guard here.
+		 */
+
+		if (IsInParallelMode() && activesnap->curcid != curcid)
+			elog(ERROR, "cannot modify commandid in active snapshot during a parallel operation");
+		activesnap->curcid = curcid;
+	}
+	else if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	{
+		HistoricMVCCSnapshot activesnap = &ActiveSnapshot->as_snap->historic_mvcc;
+
+		if (activesnap->curcid != curcid)
+			elog(ERROR, "cannot modify commandid in an active historic snapshot");
+
+	}
+	else
+		elog(ERROR, "unexpected snapshot of kind %d in active stack",
+			 ActiveSnapshot->as_snap->base.snapshot_type);
 }
 
 /*
@@ -778,13 +815,28 @@ PopActiveSnapshot(void)
 
 	newstack = ActiveSnapshot->as_next;
 
-	Assert(ActiveSnapshot->as_snap->active_count > 0);
+	if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_MVCC)
+	{
+		MVCCSnapshot snap = (MVCCSnapshot) ActiveSnapshot->as_snap;
 
-	ActiveSnapshot->as_snap->active_count--;
+		Assert(snap->active_count > 0);
+		snap->active_count--;
 
-	if (ActiveSnapshot->as_snap->active_count == 0 &&
-		ActiveSnapshot->as_snap->regd_count == 0)
-		FreeSnapshot(ActiveSnapshot->as_snap);
+		if (snap->active_count == 0 && snap->regd_count == 0)
+			FreeMVCCSnapshot(snap);
+	}
+	else if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	{
+		HistoricMVCCSnapshot snap = (HistoricMVCCSnapshot) ActiveSnapshot->as_snap;
+
+		Assert(snap->refcount > 0);
+		Assert(snap->active_count > 0);
+		snap->active_count--;
+		elog(LOG, "popped historic snap %p, count %d", snap, snap->active_count);
+	}
+	else
+		elog(ERROR, "unexpected snapshot of kind %d in active stack",
+			 ActiveSnapshot->as_snap->base.snapshot_type);
 
 	pfree(ActiveSnapshot);
 	ActiveSnapshot = newstack;
@@ -801,7 +853,7 @@ GetActiveSnapshot(void)
 {
 	Assert(ActiveSnapshot != NULL);
 
-	return ActiveSnapshot->as_snap;
+	return (Snapshot) ActiveSnapshot->as_snap;
 }
 
 /*
@@ -818,7 +870,8 @@ ActiveSnapshotSet(void)
  * RegisterSnapshot
  *		Register a snapshot as being in use by the current resource owner
  *
- * If InvalidSnapshot is passed, it is not registered.
+ * Only regular MVCC snaphots and "historic" MVCC snapshots can be registered.
+ * InvalidSnapshot is also accepted, as a no-op.
  */
 Snapshot
 RegisterSnapshot(Snapshot snapshot)
@@ -834,25 +887,39 @@ RegisterSnapshot(Snapshot snapshot)
  *		As above, but use the specified resource owner
  */
 Snapshot
-RegisterSnapshotOnOwner(Snapshot snapshot, ResourceOwner owner)
+RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 {
-	Snapshot	snap;
+	MVCCSnapshot snapshot;
 
-	if (snapshot == InvalidSnapshot)
+	if (orig_snapshot == InvalidSnapshot)
 		return InvalidSnapshot;
 
+	if (orig_snapshot->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	{
+		HistoricMVCCSnapshot historicsnap = &orig_snapshot->historic_mvcc;
+
+		ResourceOwnerEnlarge(owner);
+		historicsnap->regd_count++;
+		ResourceOwnerRememberSnapshot(owner, (Snapshot) historicsnap);
+
+		return (Snapshot) historicsnap;
+	}
+
+	Assert(orig_snapshot->base.snapshot_type == SNAPSHOT_MVCC);
+	snapshot = &orig_snapshot->mvcc;
+
 	/* Static snapshot?  Create a persistent copy */
-	snap = snapshot->copied ? snapshot : CopySnapshot(snapshot);
+	snapshot = snapshot->copied ? snapshot : CopyMVCCSnapshot(snapshot);
 
 	/* and tell resowner.c about it */
 	ResourceOwnerEnlarge(owner);
-	snap->regd_count++;
-	ResourceOwnerRememberSnapshot(owner, snap);
+	snapshot->regd_count++;
+	ResourceOwnerRememberSnapshot(owner, (Snapshot) snapshot);
 
-	if (snap->regd_count == 1)
-		pairingheap_add(&RegisteredSnapshots, &snap->ph_node);
+	if (snapshot->regd_count == 1)
+		pairingheap_add(&RegisteredSnapshots, &snapshot->ph_node);
 
-	return snap;
+	return (Snapshot) snapshot;
 }
 
 /*
@@ -888,18 +955,41 @@ UnregisterSnapshotFromOwner(Snapshot snapshot, ResourceOwner owner)
 static void
 UnregisterSnapshotNoOwner(Snapshot snapshot)
 {
-	Assert(snapshot->regd_count > 0);
-	Assert(!pairingheap_is_empty(&RegisteredSnapshots));
+	if (snapshot->base.snapshot_type == SNAPSHOT_MVCC)
+	{
+		MVCCSnapshot mvccsnap = &snapshot->mvcc;
+
+		Assert(mvccsnap->regd_count > 0);
+		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
 
-	snapshot->regd_count--;
-	if (snapshot->regd_count == 0)
-		pairingheap_remove(&RegisteredSnapshots, &snapshot->ph_node);
+		mvccsnap->regd_count--;
+		if (mvccsnap->regd_count == 0)
+			pairingheap_remove(&RegisteredSnapshots, &mvccsnap->ph_node);
 
-	if (snapshot->regd_count == 0 && snapshot->active_count == 0)
+		if (mvccsnap->regd_count == 0 && mvccsnap->active_count == 0)
+		{
+			FreeMVCCSnapshot(mvccsnap);
+			SnapshotResetXmin();
+		}
+	}
+	else if (snapshot->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
 	{
-		FreeSnapshot(snapshot);
-		SnapshotResetXmin();
+		HistoricMVCCSnapshot historicsnap = &snapshot->historic_mvcc;
+
+		/*
+		 * Historic snapshots don't rely on the resource owner machinery for
+		 * cleanup, the snapbuild.c machinery ensures that whenever a historic
+		 * snapshot is in use, it has a non-zero refcount.  Registration is
+		 * only supported so that the callers don't need to treat regular MVCC
+		 * catalog snapshots and historic snapshots differently.
+		 */
+		Assert(historicsnap->refcount > 0);
+
+		Assert(historicsnap->regd_count > 0);
+		historicsnap->regd_count--;
 	}
+	else
+		elog(ERROR, "registered snapshot has unexpected type");
 }
 
 /*
@@ -909,8 +999,8 @@ UnregisterSnapshotNoOwner(Snapshot snapshot)
 static int
 xmin_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg)
 {
-	const SnapshotData *asnap = pairingheap_const_container(SnapshotData, ph_node, a);
-	const SnapshotData *bsnap = pairingheap_const_container(SnapshotData, ph_node, b);
+	const MVCCSnapshotData *asnap = pairingheap_const_container(MVCCSnapshotData, ph_node, a);
+	const MVCCSnapshotData *bsnap = pairingheap_const_container(MVCCSnapshotData, ph_node, b);
 
 	if (TransactionIdPrecedes(asnap->xmin, bsnap->xmin))
 		return 1;
@@ -936,7 +1026,7 @@ xmin_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg)
 static void
 SnapshotResetXmin(void)
 {
-	Snapshot	minSnapshot;
+	MVCCSnapshot minSnapshot;
 
 	if (ActiveSnapshot != NULL)
 		return;
@@ -947,7 +1037,7 @@ SnapshotResetXmin(void)
 		return;
 	}
 
-	minSnapshot = pairingheap_container(SnapshotData, ph_node,
+	minSnapshot = pairingheap_container(MVCCSnapshotData, ph_node,
 										pairingheap_first(&RegisteredSnapshots));
 
 	if (TransactionIdPrecedes(MyProc->xmin, minSnapshot->xmin))
@@ -992,12 +1082,27 @@ AtSubAbort_Snapshot(int level)
 		 * Decrement the snapshot's active count.  If it's still registered or
 		 * marked as active by an outer subtransaction, we can't free it yet.
 		 */
-		Assert(ActiveSnapshot->as_snap->active_count >= 1);
-		ActiveSnapshot->as_snap->active_count -= 1;
+		if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_MVCC)
+		{
+			MVCCSnapshot snap = (MVCCSnapshot) ActiveSnapshot->as_snap;
+
+			Assert(snap->active_count > 0);
+			snap->active_count--;
+
+			if (snap->active_count == 0 && snap->regd_count == 0)
+				FreeMVCCSnapshot(snap);
+		}
+		else if (ActiveSnapshot->as_snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+		{
+			HistoricMVCCSnapshot snap = (HistoricMVCCSnapshot) ActiveSnapshot->as_snap;
 
-		if (ActiveSnapshot->as_snap->active_count == 0 &&
-			ActiveSnapshot->as_snap->regd_count == 0)
-			FreeSnapshot(ActiveSnapshot->as_snap);
+			Assert(snap->refcount > 0);
+			Assert(snap->active_count > 0);
+			snap->active_count--;
+		}
+		else
+			elog(ERROR, "unexpected snapshot of kind %d in active stack",
+				 ActiveSnapshot->as_snap->base.snapshot_type);
 
 		/* and free the stack element */
 		pfree(ActiveSnapshot);
@@ -1019,7 +1124,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * In transaction-snapshot mode we must release our privately-managed
 	 * reference to the transaction snapshot.  We must remove it from
 	 * RegisteredSnapshots to keep the check below happy.  But we don't bother
-	 * to do FreeSnapshot, for two reasons: the memory will go away with
+	 * to do FreeMVCCSnapshot, for two reasons: the memory will go away with
 	 * TopTransactionContext anyway, and if someone has left the snapshot
 	 * stacked as active, we don't want the code below to be chasing through a
 	 * dangling pointer.
@@ -1112,7 +1217,7 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
  *		snapshot.
  */
 char *
-ExportSnapshot(Snapshot snapshot)
+ExportSnapshot(MVCCSnapshot snapshot)
 {
 	TransactionId topXid;
 	TransactionId *children;
@@ -1176,7 +1281,7 @@ ExportSnapshot(Snapshot snapshot)
 	 * ensure that the snapshot's xmin is honored for the rest of the
 	 * transaction.
 	 */
-	snapshot = CopySnapshot(snapshot);
+	snapshot = CopyMVCCSnapshot(snapshot);
 
 	oldcxt = MemoryContextSwitchTo(TopTransactionContext);
 	esnap = palloc_object(ExportedSnapshot);
@@ -1293,7 +1398,7 @@ pg_export_snapshot(PG_FUNCTION_ARGS)
 {
 	char	   *snapshotName;
 
-	snapshotName = ExportSnapshot(GetActiveSnapshot());
+	snapshotName = ExportSnapshot((MVCCSnapshot) GetActiveSnapshot());
 	PG_RETURN_TEXT_P(cstring_to_text(snapshotName));
 }
 
@@ -1397,7 +1502,7 @@ ImportSnapshot(const char *idstr)
 	Oid			src_dbid;
 	int			src_isolevel;
 	bool		src_readonly;
-	SnapshotData snapshot;
+	MVCCSnapshotData snapshot;
 
 	/*
 	 * Must be at top level of a fresh transaction.  Note in particular that
@@ -1476,7 +1581,7 @@ ImportSnapshot(const char *idstr)
 	src_isolevel = parseIntFromText("iso:", &filebuf, path);
 	src_readonly = parseIntFromText("ro:", &filebuf, path);
 
-	snapshot.snapshot_type = SNAPSHOT_MVCC;
+	snapshot.base.snapshot_type = SNAPSHOT_MVCC;
 
 	snapshot.xmin = parseXidFromText("xmin:", &filebuf, path);
 	snapshot.xmax = parseXidFromText("xmax:", &filebuf, path);
@@ -1666,7 +1771,7 @@ HaveRegisteredOrActiveSnapshot(void)
  * Needed for logical decoding.
  */
 void
-SetupHistoricSnapshot(Snapshot historic_snapshot, HTAB *tuplecids)
+SetupHistoricSnapshot(HistoricMVCCSnapshot historic_snapshot, HTAB *tuplecids)
 {
 	Assert(historic_snapshot != NULL);
 
@@ -1709,12 +1814,11 @@ HistoricSnapshotGetTupleCids(void)
  * SerializedSnapshotData.
  */
 Size
-EstimateSnapshotSpace(Snapshot snapshot)
+EstimateSnapshotSpace(MVCCSnapshot snapshot)
 {
 	Size		size;
 
-	Assert(snapshot != InvalidSnapshot);
-	Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+	Assert(snapshot->base.snapshot_type == SNAPSHOT_MVCC);
 
 	/* We allocate any XID arrays needed in the same palloc block. */
 	size = add_size(sizeof(SerializedSnapshotData),
@@ -1733,12 +1837,10 @@ EstimateSnapshotSpace(Snapshot snapshot)
  *		memory location at start_address.
  */
 void
-SerializeSnapshot(Snapshot snapshot, char *start_address)
+SerializeSnapshot(MVCCSnapshot snapshot, char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 
-	if (snapshot->snapshot_type != SNAPSHOT_MVCC)
-		elog(ERROR, "cannot serialize non-MVCC snapshot");
 	Assert(snapshot->subxcnt >= 0);
 
 	/* Copy all required fields */
@@ -1791,12 +1893,12 @@ SerializeSnapshot(Snapshot snapshot, char *start_address)
  * The copy is palloc'd in TopTransactionContext and has initial refcounts set
  * to 0.  The returned snapshot has the copied flag set.
  */
-Snapshot
+MVCCSnapshot
 RestoreSnapshot(char *start_address)
 {
 	SerializedSnapshotData serialized_snapshot;
 	Size		size;
-	Snapshot	snapshot;
+	MVCCSnapshot snapshot;
 	TransactionId *serialized_xids;
 
 	memcpy(&serialized_snapshot, start_address,
@@ -1805,13 +1907,13 @@ RestoreSnapshot(char *start_address)
 		(start_address + sizeof(SerializedSnapshotData));
 
 	/* We allocate any XID arrays needed in the same palloc block. */
-	size = sizeof(SnapshotData)
+	size = sizeof(MVCCSnapshotData)
 		+ serialized_snapshot.xcnt * sizeof(TransactionId)
 		+ serialized_snapshot.subxcnt * sizeof(TransactionId);
 
 	/* Copy all required fields */
-	snapshot = (Snapshot) MemoryContextAlloc(TopTransactionContext, size);
-	snapshot->snapshot_type = SNAPSHOT_MVCC;
+	snapshot = (MVCCSnapshot) MemoryContextAlloc(TopTransactionContext, size);
+	snapshot->base.snapshot_type = SNAPSHOT_MVCC;
 	snapshot->xmin = serialized_snapshot.xmin;
 	snapshot->xmax = serialized_snapshot.xmax;
 	snapshot->xip = NULL;
@@ -1852,7 +1954,7 @@ RestoreSnapshot(char *start_address)
  * Install a restored snapshot as the transaction snapshot.
  */
 void
-RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc)
+RestoreTransactionSnapshot(MVCCSnapshot snapshot, PGPROC *source_pgproc)
 {
 	SetTransactionSnapshot(snapshot, NULL, InvalidPid, source_pgproc);
 }
@@ -1868,7 +1970,7 @@ RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc)
  * XID could not be ours anyway.
  */
 bool
-XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot)
+XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot)
 {
 	/*
 	 * Make a quick range check to eliminate most XIDs without looking at the
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index f7e4ae3843c..b64731f926c 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -455,7 +455,7 @@ extern bool HeapTupleIsSurelyDead(HeapTuple htup,
  */
 struct HTAB;
 extern bool ResolveCminCmaxDuringDecoding(struct HTAB *tuplecid_data,
-										  Snapshot snapshot,
+										  HistoricMVCCSnapshot snapshot,
 										  HeapTuple htup,
 										  Buffer buffer,
 										  CommandId *cmin, CommandId *cmax);
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 78989a959d4..0f8fdcff782 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -34,7 +34,7 @@ typedef struct TableScanDescData
 {
 	/* scan parameters */
 	Relation	rs_rd;			/* heap relation descriptor */
-	struct SnapshotData *rs_snapshot;	/* snapshot to see */
+	union SnapshotData *rs_snapshot;	/* snapshot to see */
 	int			rs_nkeys;		/* number of scan keys */
 	struct ScanKeyData *rs_key; /* array of scan key descriptors */
 
@@ -137,7 +137,7 @@ typedef struct IndexScanDescData
 	/* scan parameters */
 	Relation	heapRelation;	/* heap relation descriptor, or NULL */
 	Relation	indexRelation;	/* index relation descriptor */
-	struct SnapshotData *xs_snapshot;	/* snapshot to see */
+	union SnapshotData *xs_snapshot;	/* snapshot to see */
 	int			numberOfKeys;	/* number of index qualifier conditions */
 	int			numberOfOrderBys;	/* number of ordering operators */
 	struct ScanKeyData *keyData;	/* array of index qualifier descriptors */
@@ -212,7 +212,7 @@ typedef struct SysScanDescData
 	Relation	irel;			/* NULL if doing heap scan */
 	struct TableScanDescData *scan; /* only valid in storage-scan case */
 	struct IndexScanDescData *iscan;	/* only valid in index-scan case */
-	struct SnapshotData *snapshot;	/* snapshot to unregister at end of scan */
+	union SnapshotData *snapshot;	/* snapshot to unregister at end of scan */
 	struct TupleTableSlot *slot;
 } SysScanDescData;
 
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 3cbe106a3c7..bfe14d5144a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -127,7 +127,7 @@ typedef struct ReorderBufferChange
 		}			msg;
 
 		/* New snapshot, set when action == *_INTERNAL_SNAPSHOT */
-		Snapshot	snapshot;
+		HistoricMVCCSnapshot snapshot;
 
 		/*
 		 * New command id for existing snapshot in a catalog changing tx. Set
@@ -366,7 +366,7 @@ typedef struct ReorderBufferTXN
 	 * transaction modifies the catalog, or another catalog-modifying
 	 * transaction commits.
 	 */
-	Snapshot	base_snapshot;
+	HistoricMVCCSnapshot base_snapshot;
 	XLogRecPtr	base_snapshot_lsn;
 	dlist_node	base_snapshot_node; /* link in txns_by_base_snapshot_lsn */
 
@@ -374,7 +374,7 @@ typedef struct ReorderBufferTXN
 	 * Snapshot/CID from the previous streaming run. Only valid for already
 	 * streamed transactions (NULL/InvalidCommandId otherwise).
 	 */
-	Snapshot	snapshot_now;
+	HistoricMVCCSnapshot snapshot_now;
 	CommandId	command_id;
 
 	/*
@@ -719,7 +719,7 @@ extern void ReorderBufferQueueChange(ReorderBuffer *rb, TransactionId xid,
 									 XLogRecPtr lsn, ReorderBufferChange *change,
 									 bool toast_insert);
 extern void ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid,
-									  Snapshot snap, XLogRecPtr lsn,
+									  HistoricMVCCSnapshot snap, XLogRecPtr lsn,
 									  bool transactional, const char *prefix,
 									  Size message_size, const char *message);
 extern void ReorderBufferCommit(ReorderBuffer *rb, TransactionId xid,
@@ -743,9 +743,9 @@ extern void ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr
 extern void ReorderBufferInvalidate(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn);
 
 extern void ReorderBufferSetBaseSnapshot(ReorderBuffer *rb, TransactionId xid,
-										 XLogRecPtr lsn, Snapshot snap);
+										 XLogRecPtr lsn, HistoricMVCCSnapshot snap);
 extern void ReorderBufferAddSnapshot(ReorderBuffer *rb, TransactionId xid,
-									 XLogRecPtr lsn, Snapshot snap);
+									 XLogRecPtr lsn, HistoricMVCCSnapshot snap);
 extern void ReorderBufferAddNewCommandId(ReorderBuffer *rb, TransactionId xid,
 										 XLogRecPtr lsn, CommandId cid);
 extern void ReorderBufferAddNewTupleCids(ReorderBuffer *rb, TransactionId xid,
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 44031dcf6e3..5930ffb55a8 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -70,15 +70,15 @@ extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  XLogRecPtr two_phase_at);
 extern void FreeSnapshotBuilder(SnapBuild *builder);
 
-extern void SnapBuildSnapDecRefcount(Snapshot snap);
+extern void SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap);
 
-extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder);
+extern MVCCSnapshot SnapBuildInitialSnapshot(SnapBuild *builder);
 extern const char *SnapBuildExportSnapshot(SnapBuild *builder);
 extern void SnapBuildClearExportedSnapshot(void);
 extern void SnapBuildResetExportedSnapshotState(void);
 
 extern SnapBuildState SnapBuildCurrentState(SnapBuild *builder);
-extern Snapshot SnapBuildGetOrBuildSnapshot(SnapBuild *builder);
+extern HistoricMVCCSnapshot SnapBuildGetOrBuildSnapshot(SnapBuild *builder);
 
 extern bool SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr);
 extern XLogRecPtr SnapBuildGetTwoPhaseAt(SnapBuild *builder);
diff --git a/src/include/replication/snapbuild_internal.h b/src/include/replication/snapbuild_internal.h
index 3b915dc8793..9bed20efa31 100644
--- a/src/include/replication/snapbuild_internal.h
+++ b/src/include/replication/snapbuild_internal.h
@@ -74,7 +74,7 @@ struct SnapBuild
 	/*
 	 * Snapshot that's valid to see the catalog state seen at this moment.
 	 */
-	Snapshot	snapshot;
+	HistoricMVCCSnapshot snapshot;
 
 	/*
 	 * LSN of the last location we are sure a snapshot has been serialized to.
diff --git a/src/include/storage/predicate.h b/src/include/storage/predicate.h
index 8f5f0348a23..9a7f53c900c 100644
--- a/src/include/storage/predicate.h
+++ b/src/include/storage/predicate.h
@@ -47,8 +47,8 @@ extern void CheckPointPredicate(void);
 extern bool PageIsPredicateLocked(Relation relation, BlockNumber blkno);
 
 /* predicate lock maintenance */
-extern Snapshot GetSerializableTransactionSnapshot(Snapshot snapshot);
-extern void SetSerializableTransactionSnapshot(Snapshot snapshot,
+extern MVCCSnapshot GetSerializableTransactionSnapshot(MVCCSnapshot snapshot);
+extern void SetSerializableTransactionSnapshot(MVCCSnapshot snapshot,
 											   VirtualTransactionId *sourcevxid,
 											   int sourcepid);
 extern void RegisterPredicateLockingXid(TransactionId xid);
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index 2f4ae06c279..c40c2ee5d1b 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -44,7 +44,7 @@ extern void KnownAssignedTransactionIdsIdleMaintenance(void);
 extern int	GetMaxSnapshotXidCount(void);
 extern int	GetMaxSnapshotSubxidCount(void);
 
-extern Snapshot GetSnapshotData(Snapshot snapshot);
+extern MVCCSnapshot GetSnapshotData(MVCCSnapshot snapshot);
 
 extern bool ProcArrayInstallImportedXmin(TransactionId xmin,
 										 VirtualTransactionId *sourcevxid);
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index b663d3bbc8c..057b1a2f8b8 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -25,22 +25,22 @@ extern PGDLLIMPORT TransactionId TransactionXmin;
 extern PGDLLIMPORT TransactionId RecentXmin;
 
 /* Variables representing various special snapshot semantics */
-extern PGDLLIMPORT SnapshotData SnapshotSelfData;
-extern PGDLLIMPORT SnapshotData SnapshotAnyData;
-extern PGDLLIMPORT SnapshotData SnapshotToastData;
+extern PGDLLIMPORT SnapshotBaseData SnapshotSelfData;
+extern PGDLLIMPORT SnapshotBaseData SnapshotAnyData;
+extern PGDLLIMPORT SnapshotBaseData SnapshotToastData;
 
-#define SnapshotSelf		(&SnapshotSelfData)
-#define SnapshotAny			(&SnapshotAnyData)
+#define SnapshotSelf		((Snapshot) &SnapshotSelfData)
+#define SnapshotAny			((Snapshot) &SnapshotAnyData)
 
 /* Use get_toast_snapshot() for the TOAST snapshot */
 
 /*
  * We don't provide a static SnapshotDirty variable because it would be
  * non-reentrant.  Instead, users of that snapshot type should declare a
- * local variable of type SnapshotData, and initialize it with this macro.
+ * local variable of type DirtySnapshotData, and initialize it with this macro.
  */
 #define InitDirtySnapshot(snapshotdata)  \
-	((snapshotdata).snapshot_type = SNAPSHOT_DIRTY)
+	((snapshotdata).base.snapshot_type = SNAPSHOT_DIRTY)
 
 /*
  * Similarly, some initialization is required for a NonVacuumable snapshot.
@@ -48,16 +48,8 @@ extern PGDLLIMPORT SnapshotData SnapshotToastData;
  * GlobalVisTestFor()).
  */
 #define InitNonVacuumableSnapshot(snapshotdata, vistestp)  \
-	((snapshotdata).snapshot_type = SNAPSHOT_NON_VACUUMABLE, \
-	 (snapshotdata).vistest = (vistestp))
-
-/* This macro encodes the knowledge of which snapshots are MVCC-safe */
-#define IsMVCCSnapshot(snapshot)  \
-	((snapshot)->snapshot_type == SNAPSHOT_MVCC || \
-	 (snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
-
-#define IsHistoricMVCCSnapshot(snapshot)  \
-	((snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+	((snapshotdata).base.snapshot_type = SNAPSHOT_NON_VACUUMABLE, \
+	 (snapshotdata).nonvacuumable.vistest = (vistestp))
 
 extern Snapshot GetTransactionSnapshot(void);
 extern Snapshot GetLatestSnapshot(void);
@@ -92,7 +84,7 @@ extern void WaitForOlderSnapshots(TransactionId limitXmin, bool progress);
 extern bool ThereAreNoPriorRegisteredSnapshots(void);
 extern bool HaveRegisteredOrActiveSnapshot(void);
 
-extern char *ExportSnapshot(Snapshot snapshot);
+extern char *ExportSnapshot(MVCCSnapshot snapshot);
 
 /*
  * These live in procarray.c because they're intimately linked to the
@@ -108,19 +100,19 @@ extern bool GlobalVisCheckRemovableFullXid(Relation rel, FullTransactionId fxid)
 /*
  * Utility functions for implementing visibility routines in table AMs.
  */
-extern bool XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot);
+extern bool XidInMVCCSnapshot(TransactionId xid, MVCCSnapshot snapshot);
 
 /* Support for catalog timetravel for logical decoding */
 struct HTAB;
 extern struct HTAB *HistoricSnapshotGetTupleCids(void);
-extern void SetupHistoricSnapshot(Snapshot historic_snapshot, struct HTAB *tuplecids);
+extern void SetupHistoricSnapshot(HistoricMVCCSnapshot historic_snapshot, struct HTAB *tuplecids);
 extern void TeardownHistoricSnapshot(bool is_error);
 extern bool HistoricSnapshotActive(void);
 
-extern Size EstimateSnapshotSpace(Snapshot snapshot);
-extern void SerializeSnapshot(Snapshot snapshot, char *start_address);
-extern Snapshot RestoreSnapshot(char *start_address);
+extern Size EstimateSnapshotSpace(MVCCSnapshot snapshot);
+extern void SerializeSnapshot(MVCCSnapshot snapshot, char *start_address);
+extern MVCCSnapshot RestoreSnapshot(char *start_address);
 struct PGPROC;
-extern void RestoreTransactionSnapshot(Snapshot snapshot, struct PGPROC *source_pgproc);
+extern void RestoreTransactionSnapshot(MVCCSnapshot snapshot, struct PGPROC *source_pgproc);
 
 #endif							/* SNAPMGR_H */
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 0e546ec1497..5f67d32feed 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -17,7 +17,7 @@
 
 
 /*
- * The different snapshot types.  We use SnapshotData structures to represent
+ * The different snapshot types.  We use the SnapshotData union to represent
  * both "regular" (MVCC) snapshots and "special" snapshots that have non-MVCC
  * semantics.  The specific semantics of a snapshot are encoded by its type.
  *
@@ -27,6 +27,9 @@
  * The reason the snapshot type rather than a callback as it used to be is
  * that that allows to use the same snapshot for different table AMs without
  * having one callback per AM.
+ *
+ * The executor deals with MVCC snapshots, but the table AM and some other
+ * parts of the system also support the special snapshots.
  */
 typedef enum SnapshotType
 {
@@ -85,14 +88,7 @@ typedef enum SnapshotType
 	 * A special hack is that when a snapshot of this type is used to
 	 * determine tuple visibility, the passed-in snapshot struct is used as an
 	 * output argument to return the xids of concurrent xacts that affected
-	 * the tuple.  snapshot->xmin is set to the tuple's xmin if that is
-	 * another transaction that's still in progress; or to
-	 * InvalidTransactionId if the tuple's xmin is committed good, committed
-	 * dead, or my own xact.  Similarly for snapshot->xmax and the tuple's
-	 * xmax.  If the tuple was inserted speculatively, meaning that the
-	 * inserter might still back down on the insertion without aborting the
-	 * whole transaction, the associated token is also returned in
-	 * snapshot->speculativeToken.  See also InitDirtySnapshot().
+	 * the tuple.  See HeapTupleSatisfiesDirty().
 	 * -------------------------------------------------------------------------
 	 */
 	SNAPSHOT_DIRTY,
@@ -100,7 +96,9 @@ typedef enum SnapshotType
 	/*
 	 * A tuple is visible iff it follows the rules of SNAPSHOT_MVCC, but
 	 * supports being called in timetravel context (for decoding catalog
-	 * contents in the context of logical decoding).
+	 * contents in the context of logical decoding).  A historic MVCC snapshot
+	 * should only be used on catalog tables, as we only track XIDs that
+	 * modify catalogs during logical decoding.
 	 */
 	SNAPSHOT_HISTORIC_MVCC,
 
@@ -114,37 +112,42 @@ typedef enum SnapshotType
 	SNAPSHOT_NON_VACUUMABLE,
 } SnapshotType;
 
-typedef struct SnapshotData *Snapshot;
 
-#define InvalidSnapshot		((Snapshot) NULL)
+/* This macro encodes the knowledge of which snapshots are MVCC-safe */
+#define IsMVCCSnapshot(snapshot)  \
+	((snapshot)->base.snapshot_type == SNAPSHOT_MVCC || \
+	 (snapshot)->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
+
+#define IsHistoricMVCCSnapshot(snapshot)  \
+	((snapshot)->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC)
 
 /*
- * Struct representing all kind of possible snapshots.
- *
- * There are several different kinds of snapshots:
- * * Normal MVCC snapshots
- * * MVCC snapshots taken during recovery (in Hot-Standby mode)
- * * Historic MVCC snapshots used during logical decoding
- * * snapshots passed to HeapTupleSatisfiesDirty()
- * * snapshots passed to HeapTupleSatisfiesNonVacuumable()
- * * snapshots used for SatisfiesAny, Toast, Self where no members are
- *	 accessed.
- *
- * TODO: It's probably a good idea to split this struct using a NodeTag
- * similar to how parser and executor nodes are handled, with one type for
- * each different kind of snapshot to avoid overloading the meaning of
- * individual fields.
+ * Convenience macro to check the 'takenDuringRecovery' flag, when you're not
+ * necessarily dealing with an MVCC snapshot.  Returns false for all non-MVCC
+ * snapshots, also for historic MVCC snapshots.
  */
-typedef struct SnapshotData
+#define IsSnapshotTakenDuringRecovery(snapshot)	\
+	((snapshot)->base.snapshot_type == SNAPSHOT_MVCC ? \
+	 (snapshot)->mvcc.takenDuringRecovery : false)
+
+/* fields common to all snapshot types */
+typedef struct SnapshotBaseData
 {
 	SnapshotType snapshot_type; /* type of snapshot */
+} SnapshotBaseData;
+
+/*
+ * Struct representing a normal MVCC snapshot.
+ *
+ * MVCC snapshots come in two variants: those taken during recovery in hot
+ * standby mode, and "normal" MVCC snapshots.  They are distinguished by
+ * takenDuringRecovery.
+ */
+typedef struct MVCCSnapshotData
+{
+	SnapshotBaseData base;		/* must be first */
 
 	/*
-	 * The remaining fields are used only for MVCC snapshots, and are normally
-	 * just zeroes in special snapshots.  (But xmin and xmax are used
-	 * specially by HeapTupleSatisfiesDirty, and xmin is used specially by
-	 * HeapTupleSatisfiesNonVacuumable.)
-	 *
 	 * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see
 	 * the effects of all older XIDs except those listed in the snapshot. xmin
 	 * is stored as an optimization to avoid needing to search the XID arrays
@@ -154,10 +157,8 @@ typedef struct SnapshotData
 	TransactionId xmax;			/* all XID >= xmax are invisible to me */
 
 	/*
-	 * For normal MVCC snapshot this contains the all xact IDs that are in
-	 * progress, unless the snapshot was taken during recovery in which case
-	 * it's empty. For historic MVCC snapshots, the meaning is inverted, i.e.
-	 * it contains *committed* transactions between xmin and xmax.
+	 * xip contains the all xact IDs that are in progress, unless the snapshot
+	 * was taken during recovery in which case it's empty.
 	 *
 	 * note: all ids in xip[] satisfy xmin <= xip[i] < xmax
 	 */
@@ -165,10 +166,8 @@ typedef struct SnapshotData
 	uint32		xcnt;			/* # of xact ids in xip[] */
 
 	/*
-	 * For non-historic MVCC snapshots, this contains subxact IDs that are in
-	 * progress (and other transactions that are in progress if taken during
-	 * recovery). For historic snapshot it contains *all* xids assigned to the
-	 * replayed transaction, including the toplevel xid.
+	 * subxip contains subxact IDs that are in progress (and other
+	 * transactions that are in progress if taken during recovery).
 	 *
 	 * note: all ids in subxip[] are >= xmin, but we don't bother filtering
 	 * out any that are >= xmax
@@ -182,18 +181,6 @@ typedef struct SnapshotData
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-	/*
-	 * An extra return value for HeapTupleSatisfiesDirty, not used in MVCC
-	 * snapshots.
-	 */
-	uint32		speculativeToken;
-
-	/*
-	 * For SNAPSHOT_NON_VACUUMABLE (and hopefully more in the future) this is
-	 * used to determine whether row could be vacuumed.
-	 */
-	struct GlobalVisState *vistest;
-
 	/*
 	 * Book-keeping information, used by the snapshot manager
 	 */
@@ -207,6 +194,100 @@ typedef struct SnapshotData
 	 * transactions completed since the last GetSnapshotData().
 	 */
 	uint64		snapXactCompletionCount;
+} MVCCSnapshotData;
+
+typedef struct MVCCSnapshotData *MVCCSnapshot;
+
+#define InvalidMVCCSnapshot ((MVCCSnapshot) NULL)
+
+/*
+ * Struct representing a "historic" MVCC snapshot during logical decoding.
+ * These are constructed by src/replication/logical/snapbuild.c.
+ */
+typedef struct HistoricMVCCSnapshotData
+{
+	SnapshotBaseData base;		/* must be first */
+
+	/*
+	 * xmin and xmax like in a normal MVCC snapshot.
+	 */
+	TransactionId xmin;			/* all XID < xmin are visible to me */
+	TransactionId xmax;			/* all XID >= xmax are invisible to me */
+
+	/*
+	 * committed_xids contains *committed* transactions between xmin and xmax.
+	 * (This is the inverse of 'xip' in normal MVCC snapshots, which contains
+	 * all non-committed transactions.)  The array is sorted by XID to allow
+	 * binary search.
+	 *
+	 * note: all ids in committed_xids[] satisfy xmin <= committed_xids[i] <
+	 * xmax
+	 */
+	TransactionId *committed_xids;
+	uint32		xcnt;			/* # of xact ids in committed_xids[] */
+
+	/*
+	 * curxip contains *all* xids assigned to the replayed transaction,
+	 * including the toplevel xid.
+	 */
+	TransactionId *curxip;
+	int32		curxcnt;		/* # of xact ids in curxip[] */
+
+	CommandId	curcid;			/* in my xact, CID < curcid are visible */
+
+	bool		copied;			/* false if it's a "base" snapshot */
+
+	uint32		refcount;		/* refcount managed by snapbuild.c  */
+	uint32		active_count;	/* refcount on ActiveSnapshot stack */
+	uint32		regd_count;		/* refcount registered with resource owners */
+
+} HistoricMVCCSnapshotData;
+
+typedef struct HistoricMVCCSnapshotData *HistoricMVCCSnapshot;
+
+/*
+ * Struct representing a special "snapshot" which sees all tuples as visible
+ * if they are visible to anyone, i.e. if they are not vacuumable.
+ * i.e. SNAPSHOT_NON_VACUUMABLE.
+ */
+typedef struct NonVacuumableSnapshotData
+{
+	SnapshotBaseData base;		/* must be first */
+
+	/* This is used to determine whether row could be vacuumed. */
+	struct GlobalVisState *vistest;
+} NonVacuumableSnapshotData;
+
+/*
+ * Return values to the caller of HeapTupleSatisfyDirty.
+ */
+typedef struct DirtySnapshotData
+{
+	SnapshotBaseData base;		/* must be first */
+
+	TransactionId updating_xmin;
+	TransactionId updating_xmax;
+	uint32		speculative_token;
+} DirtySnapshotData;
+
+/*
+ * Generic union representing all kind of possible snapshots.
+ *
+ * 'base' contains the fields common to all snapshots.  Some snapshot types
+ * have type-specific structs with more fields.
+ */
+typedef union SnapshotData
+{
+	SnapshotBaseData base;
+
+	MVCCSnapshotData mvcc;
+	DirtySnapshotData dirty;
+	HistoricMVCCSnapshotData historic_mvcc;
+	NonVacuumableSnapshotData nonvacuumable;
 } SnapshotData;
 
+typedef union SnapshotData *Snapshot;
+
+#define InvalidSnapshot		((Snapshot) NULL)
+
 #endif							/* SNAPSHOT_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 04845d5e680..3ffccb2fa14 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -656,6 +656,7 @@ DictThesaurus
 DimensionInfo
 DirectoryMethodData
 DirectoryMethodFile
+DirtySnapshotData
 DisableTimeoutParams
 DiscardMode
 DiscardStmt
@@ -1217,6 +1218,7 @@ HeapTupleFreeze
 HeapTupleHeader
 HeapTupleHeaderData
 HeapTupleTableSlot
+HistoricMVCCSnapshotData
 HistControl
 HotStandbyState
 I32
@@ -1673,6 +1675,7 @@ MINIDUMPWRITEDUMP
 MINIDUMP_TYPE
 MJEvalResult
 MTTargetRelLookup
+MVCCSnapshotData
 MVDependencies
 MVDependency
 MVNDistinct
@@ -1779,6 +1782,7 @@ Node
 NodeTag
 NonEmptyRange
 NoneCompressorState
+NonVacuumableSnapshotData
 Notification
 NotificationList
 NotifyStmt
@@ -2840,6 +2844,7 @@ SnapBuild
 SnapBuildOnDisk
 SnapBuildState
 Snapshot
+SnapshotBaseData
 SnapshotData
 SnapshotType
 SockAddr
-- 
2.47.3



  [text/x-patch] v2-0003-Simplify-historic-snapshot-refcounting.patch (13.3K, ../../[email protected]/4-v2-0003-Simplify-historic-snapshot-refcounting.patch)
  download | inline diff:
From 62d73fa115816c34cce0612fa828a729579f42da Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 13 Mar 2025 16:45:12 +0200
Subject: [PATCH v2 3/5] Simplify historic snapshot refcounting

ReorderBufferProcessTXN() handled "copied" snapshots created with
ReorderBufferCopySnap() differently from "base" historic snapshots
created by snapbuild.c. The base snapshots used a reference count,
while copied snapshots did not. Simplify by using the reference count
for both.
---
 .../replication/logical/reorderbuffer.c       | 97 ++++++++-----------
 src/backend/replication/logical/snapbuild.c   | 48 +--------
 src/include/replication/snapbuild.h           |  1 +
 src/include/utils/snapshot.h                  |  2 -
 4 files changed, 46 insertions(+), 102 deletions(-)

diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index ba032f18c3f..c65ccff5668 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -103,7 +103,7 @@
 #include "replication/logical.h"
 #include "replication/reorderbuffer.h"
 #include "replication/slot.h"
-#include "replication/snapbuild.h"	/* just for SnapBuildSnapDecRefcount */
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/procarray.h"
@@ -280,7 +280,6 @@ static void ReorderBufferSerializedPath(char *path, ReplicationSlot *slot,
 										TransactionId xid, XLogSegNo segno);
 static int	ReorderBufferTXNSizeCompare(const pairingheap_node *a, const pairingheap_node *b, void *arg);
 
-static void ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap);
 static HistoricMVCCSnapshot ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 												  ReorderBufferTXN *txn, CommandId cid);
 
@@ -562,7 +561,7 @@ ReorderBufferFreeChange(ReorderBuffer *rb, ReorderBufferChange *change,
 		case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 			if (change->data.snapshot)
 			{
-				ReorderBufferFreeSnap(rb, change->data.snapshot);
+				SnapBuildSnapDecRefcount(change->data.snapshot);
 				change->data.snapshot = NULL;
 			}
 			break;
@@ -1612,7 +1611,8 @@ ReorderBufferCleanupTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	if (txn->snapshot_now != NULL)
 	{
 		Assert(rbtxn_is_streamed(txn));
-		ReorderBufferFreeSnap(rb, txn->snapshot_now);
+		SnapBuildSnapDecRefcount(txn->snapshot_now);
+		txn->snapshot_now = NULL;
 	}
 
 	/*
@@ -1921,7 +1921,6 @@ ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 	snap = MemoryContextAllocZero(rb->context, size);
 	memcpy(snap, orig_snap, sizeof(HistoricMVCCSnapshotData));
 
-	snap->copied = true;
 	snap->refcount = 1;			/* mark as active so nobody frees it */
 	snap->active_count = 0;
 	snap->regd_count = 0;
@@ -1962,18 +1961,6 @@ ReorderBufferCopySnap(ReorderBuffer *rb, HistoricMVCCSnapshot orig_snap,
 	return snap;
 }
 
-/*
- * Free a previously ReorderBufferCopySnap'ed snapshot
- */
-static void
-ReorderBufferFreeSnap(ReorderBuffer *rb, HistoricMVCCSnapshot snap)
-{
-	if (snap->copied)
-		pfree(snap);
-	else
-		SnapBuildSnapDecRefcount(snap);
-}
-
 /*
  * If the transaction was (partially) streamed, we need to prepare or commit
  * it in a 'streamed' way.  That is, we first stream the remaining part of the
@@ -2124,11 +2111,8 @@ ReorderBufferSaveTXNSnapshot(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	txn->command_id = command_id;
 
 	/* Avoid copying if it's already copied. */
-	if (snapshot_now->copied)
-		txn->snapshot_now = snapshot_now;
-	else
-		txn->snapshot_now = ReorderBufferCopySnap(rb, snapshot_now,
-												  txn, command_id);
+	txn->snapshot_now = snapshot_now;
+	SnapBuildSnapIncRefcount(txn->snapshot_now);
 }
 
 /*
@@ -2229,6 +2213,8 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 	/* setup the initial snapshot */
 	SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
+	/* increase refcount for the installed historic snapshot */
+	SnapBuildSnapIncRefcount(snapshot_now);
 
 	/*
 	 * Decoding needs access to syscaches et al., which in turn use
@@ -2532,33 +2518,12 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				case REORDER_BUFFER_CHANGE_INTERNAL_SNAPSHOT:
 					/* get rid of the old */
 					TeardownHistoricSnapshot(false);
-
-					if (snapshot_now->copied)
-					{
-						ReorderBufferFreeSnap(rb, snapshot_now);
-						snapshot_now =
-							ReorderBufferCopySnap(rb, change->data.snapshot,
-												  txn, command_id);
-					}
-
-					/*
-					 * Restored from disk, need to be careful not to double
-					 * free. We could introduce refcounting for that, but for
-					 * now this seems infrequent enough not to care.
-					 */
-					else if (change->data.snapshot->copied)
-					{
-						snapshot_now =
-							ReorderBufferCopySnap(rb, change->data.snapshot,
-												  txn, command_id);
-					}
-					else
-					{
-						snapshot_now = change->data.snapshot;
-					}
+					SnapBuildSnapDecRefcount(snapshot_now);
 
 					/* and continue with the new one */
+					snapshot_now = change->data.snapshot;
 					SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
+					SnapBuildSnapIncRefcount(snapshot_now);
 					break;
 
 				case REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID:
@@ -2568,16 +2533,26 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 					{
 						command_id = change->data.command_id;
 
-						if (!snapshot_now->copied)
+						TeardownHistoricSnapshot(false);
+
+						/*
+						 * Construct a new snapshot with the new command ID.
+						 *
+						 * If this is the only reference to the snapshot, and
+						 * it's a "copied" snapshot that already contains all
+						 * the replayed transaction's XIDs (curxnct > 0), we
+						 * can take a shortcut and update the snapshot's
+						 * command ID in place.
+						 */
+						if (snapshot_now->refcount == 1 && snapshot_now->curxcnt > 0)
+							snapshot_now->curcid = command_id;
+						else
 						{
-							/* we don't use the global one anymore */
+							SnapBuildSnapDecRefcount(snapshot_now);
 							snapshot_now = ReorderBufferCopySnap(rb, snapshot_now,
 																 txn, command_id);
 						}
 
-						snapshot_now->curcid = command_id;
-
-						TeardownHistoricSnapshot(false);
 						SetupHistoricSnapshot(snapshot_now, txn->tuplecid_hash);
 					}
 
@@ -2667,11 +2642,11 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		 */
 		if (streaming)
 			ReorderBufferSaveTXNSnapshot(rb, txn, snapshot_now, command_id);
-		else if (snapshot_now->copied)
-			ReorderBufferFreeSnap(rb, snapshot_now);
 
 		/* cleanup */
 		TeardownHistoricSnapshot(false);
+		SnapBuildSnapDecRefcount(snapshot_now);
+		snapshot_now = NULL;
 
 		/*
 		 * Aborting the current (sub-)transaction as a whole has the right
@@ -2738,6 +2713,11 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 		TeardownHistoricSnapshot(true);
 
+		/*
+		 * don't decrement the refcount on snapshot_now yet, we still use it
+		 * in the ReorderBufferResetTXN() call below.
+		 */
+
 		/*
 		 * Force cache invalidation to happen outside of a valid transaction
 		 * to prevent catalog access as we just caught an error.
@@ -2799,9 +2779,15 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 			ReorderBufferResetTXN(rb, txn, snapshot_now,
 								  command_id, prev_lsn,
 								  specinsert);
+
+			SnapBuildSnapDecRefcount(snapshot_now);
+			snapshot_now = NULL;
 		}
 		else
 		{
+			SnapBuildSnapDecRefcount(snapshot_now);
+			snapshot_now = NULL;
+
 			ReorderBufferCleanupTXN(rb, txn);
 			MemoryContextSwitchTo(ecxt);
 			PG_RE_THROW();
@@ -4421,8 +4407,7 @@ ReorderBufferStreamTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 											 txn, command_id);
 
 		/* Free the previously copied snapshot. */
-		Assert(txn->snapshot_now->copied);
-		ReorderBufferFreeSnap(rb, txn->snapshot_now);
+		SnapBuildSnapDecRefcount(txn->snapshot_now);
 		txn->snapshot_now = NULL;
 	}
 
@@ -4812,7 +4797,7 @@ ReorderBufferRestoreChange(ReorderBuffer *rb, ReorderBufferTXN *txn,
 				newsnap->committed_xids = (TransactionId *)
 					(((char *) newsnap) + sizeof(HistoricMVCCSnapshotData));
 				newsnap->curxip = newsnap->committed_xids + newsnap->xcnt;
-				newsnap->copied = true;
+				newsnap->refcount = 1;
 				break;
 			}
 			/* the base struct contains all the data, easy peasy */
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index a4d2288961b..f0d77c9fe49 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -157,10 +157,6 @@ static void SnapBuildPurgeOlderTxn(SnapBuild *builder);
 /* snapshot building/manipulation/distribution functions */
 static HistoricMVCCSnapshot SnapBuildBuildSnapshot(SnapBuild *builder);
 
-static void SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap);
-
-static void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
-
 static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
 
 static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
@@ -245,31 +241,6 @@ FreeSnapshotBuilder(SnapBuild *builder)
 	MemoryContextDelete(context);
 }
 
-/*
- * Free an unreferenced snapshot that has previously been built by us.
- */
-static void
-SnapBuildFreeSnapshot(HistoricMVCCSnapshot snap)
-{
-	/* make sure we don't get passed an external snapshot */
-	Assert(snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC);
-
-	/* make sure nobody modified our snapshot */
-	Assert(snap->curcid == FirstCommandId);
-	Assert(snap->regd_count == 0);
-
-	/* slightly more likely, so it's checked even without c-asserts */
-	if (snap->copied)
-		elog(ERROR, "cannot free a copied snapshot");
-
-	if (snap->active_count)
-		elog(ERROR, "cannot free an active snapshot");
-	if (snap->refcount)
-		elog(ERROR, "cannot free a snapshot that's in use");
-
-	pfree(snap);
-}
-
 /*
  * In which state of snapshot building are we?
  */
@@ -312,7 +283,7 @@ SnapBuildXactNeedsSkip(SnapBuild *builder, XLogRecPtr ptr)
  * This is used when handing out a snapshot to some external resource or when
  * adding a Snapshot as builder->snapshot.
  */
-static void
+void
 SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 {
 	snap->refcount++;
@@ -320,9 +291,6 @@ SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap)
 
 /*
  * Decrease refcount of a snapshot and free if the refcount reaches zero.
- *
- * Externally visible, so that external resources that have been handed an
- * IncRef'ed Snapshot can adjust its refcount easily.
  */
 void
 SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
@@ -330,21 +298,16 @@ SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap)
 	/* make sure we don't get passed an external snapshot */
 	Assert(snap->base.snapshot_type == SNAPSHOT_HISTORIC_MVCC);
 
-	/* make sure nobody modified our snapshot */
-	Assert(snap->curcid == FirstCommandId);
-
 	Assert(snap->refcount > 0);
 	Assert(snap->regd_count == 0);
 
 	/* slightly more likely, so it's checked even without casserts */
-	if (snap->copied)
-		elog(ERROR, "cannot free a copied snapshot");
 	if (snap->active_count)
 		elog(ERROR, "cannot free an active snapshot");
 
 	snap->refcount--;
 	if (snap->refcount == 0)
-		SnapBuildFreeSnapshot(snap);
+		pfree(snap);
 }
 
 /*
@@ -417,7 +380,6 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
 	snapshot->curxcnt = 0;
 	snapshot->curxip = NULL;
 
-	snapshot->copied = false;
 	snapshot->curcid = FirstCommandId;
 	snapshot->refcount = 0;
 	snapshot->active_count = 0;
@@ -1087,18 +1049,16 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
 			SnapBuildSnapDecRefcount(builder->snapshot);
 
 		builder->snapshot = SnapBuildBuildSnapshot(builder);
+		SnapBuildSnapIncRefcount(builder->snapshot);
 
 		/* we might need to execute invalidations, add snapshot */
 		if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, xid))
 		{
-			SnapBuildSnapIncRefcount(builder->snapshot);
 			ReorderBufferSetBaseSnapshot(builder->reorder, xid, lsn,
 										 builder->snapshot);
+			SnapBuildSnapIncRefcount(builder->snapshot);
 		}
 
-		/* refcount of the snapshot builder for the new snapshot */
-		SnapBuildSnapIncRefcount(builder->snapshot);
-
 		/*
 		 * Add a new catalog snapshot and invalidations messages to all
 		 * currently running transactions.
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 5930ffb55a8..6095013a299 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -70,6 +70,7 @@ extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder,
 										  XLogRecPtr two_phase_at);
 extern void FreeSnapshotBuilder(SnapBuild *builder);
 
+extern void SnapBuildSnapIncRefcount(HistoricMVCCSnapshot snap);
 extern void SnapBuildSnapDecRefcount(HistoricMVCCSnapshot snap);
 
 extern MVCCSnapshot SnapBuildInitialSnapshot(SnapBuild *builder);
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 5f67d32feed..61f675f86b6 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -235,8 +235,6 @@ typedef struct HistoricMVCCSnapshotData
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-	bool		copied;			/* false if it's a "base" snapshot */
-
 	uint32		refcount;		/* refcount managed by snapbuild.c  */
 	uint32		active_count;	/* refcount on ActiveSnapshot stack */
 	uint32		regd_count;		/* refcount registered with resource owners */
-- 
2.47.3



  [text/x-patch] v2-0004-Add-an-explicit-valid-flag-to-MVCCSnapshotData.patch (4.0K, ../../[email protected]/5-v2-0004-Add-an-explicit-valid-flag-to-MVCCSnapshotData.patch)
  download | inline diff:
From 4b3f741562590978b1bd1ed75152c08f922e22d3 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 23:47:48 +0300
Subject: [PATCH v2 4/5] Add an explicit 'valid' flag to MVCCSnapshotData

The lifetime of the "static" snapshots returned by
GetTransactionSnapshot(), GetLatestSnapshot() and GetCatalogSnapshot()
is a bit vague. By adding an explicit 'valid' flag, we can make it
more clear when a function call updates a static snapshot, making it
valid, and when another function makes it invalid again. It's
currently only used in assertions, and can also be handy when
debugging.
---
 src/backend/storage/ipc/procarray.c |  2 ++
 src/backend/utils/time/snapmgr.c    | 16 ++++++++++++++++
 src/include/utils/snapshot.h        |  1 +
 3 files changed, 19 insertions(+)

diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 2292d8a09af..29a743ee49c 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -2083,6 +2083,7 @@ GetSnapshotDataReuse(MVCCSnapshot snapshot)
 	snapshot->active_count = 0;
 	snapshot->regd_count = 0;
 	snapshot->copied = false;
+	snapshot->valid = true;
 
 	return true;
 }
@@ -2462,6 +2463,7 @@ GetSnapshotData(MVCCSnapshot snapshot)
 	snapshot->active_count = 0;
 	snapshot->regd_count = 0;
 	snapshot->copied = false;
+	snapshot->valid = true;
 
 	return snapshot;
 }
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index f16f3fd48e5..ecddb7f97da 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -459,6 +459,7 @@ InvalidateCatalogSnapshot(void)
 	{
 		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
 		CatalogSnapshot = NULL;
+		CatalogSnapshotData.valid = false;
 		SnapshotResetXmin();
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
 	}
@@ -624,6 +625,7 @@ CopyMVCCSnapshot(MVCCSnapshot snapshot)
 	newsnap->regd_count = 0;
 	newsnap->active_count = 0;
 	newsnap->copied = true;
+	newsnap->valid = true;
 	newsnap->snapXactCompletionCount = 0;
 
 	/* setup XID array */
@@ -665,6 +667,7 @@ FreeMVCCSnapshot(MVCCSnapshot snapshot)
 	Assert(snapshot->regd_count == 0);
 	Assert(snapshot->active_count == 0);
 	Assert(snapshot->copied);
+	Assert(snapshot->valid);
 
 	pfree(snapshot);
 }
@@ -710,6 +713,8 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 		MVCCSnapshot origsnap = &snapshot->mvcc;
 		MVCCSnapshot newsnap;
 
+		Assert(origsnap->valid);
+
 		/*
 		 * Checking SecondarySnapshot is probably useless here, but it seems
 		 * better to be sure.
@@ -907,6 +912,7 @@ RegisterSnapshotOnOwner(Snapshot orig_snapshot, ResourceOwner owner)
 
 	Assert(orig_snapshot->base.snapshot_type == SNAPSHOT_MVCC);
 	snapshot = &orig_snapshot->mvcc;
+	Assert(snapshot->valid);
 
 	/* Static snapshot?  Create a persistent copy */
 	snapshot = snapshot->copied ? snapshot : CopyMVCCSnapshot(snapshot);
@@ -1028,6 +1034,15 @@ SnapshotResetXmin(void)
 {
 	MVCCSnapshot minSnapshot;
 
+	/*
+	 * These static snapshots are not in the RegisteredSnapshots list, so we
+	 * might advance MyProc->xmin past their xmin. (Note that in case of
+	 * IsolationUsesXactSnapshot() == true, CurrentSnapshot points to the copy
+	 * in FirstSnapshot rather than CurrentSnapshotData.)
+	 */
+	CurrentSnapshotData.valid = false;
+	SecondarySnapshotData.valid = false;
+
 	if (ActiveSnapshot != NULL)
 		return;
 
@@ -1946,6 +1961,7 @@ RestoreSnapshot(char *start_address)
 	snapshot->regd_count = 0;
 	snapshot->active_count = 0;
 	snapshot->copied = true;
+	snapshot->valid = true;
 
 	return snapshot;
 }
diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h
index 61f675f86b6..ff07ec5aa51 100644
--- a/src/include/utils/snapshot.h
+++ b/src/include/utils/snapshot.h
@@ -178,6 +178,7 @@ typedef struct MVCCSnapshotData
 
 	bool		takenDuringRecovery;	/* recovery-shaped snapshot? */
 	bool		copied;			/* false if it's a static snapshot */
+	bool		valid;			/* is this snapshot valid? */
 
 	CommandId	curcid;			/* in my xact, CID < curcid are visible */
 
-- 
2.47.3



  [text/x-patch] v2-0005-Replace-static-snapshot-pointers-with-the-valid-f.patch (14.5K, ../../[email protected]/6-v2-0005-Replace-static-snapshot-pointers-with-the-valid-f.patch)
  download | inline diff:
From 391ba2f4de408f5ea97d99d7e22f1dbb17b0206c Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 31 Mar 2025 21:44:43 +0300
Subject: [PATCH v2 5/5] Replace static snapshot pointers with the 'valid'
 flags

Previously, we used the pointers like SecondarySnapshot and
CatalogSnapshot to indicate whether the corresponding static snapshot
is valid or not, but now that we have an explicit flag in
MVCCSnapshotData for that, we replace checks like "SecondarySnapshot
!= NULL" with "SecondarySnapshotData.valid", and get rid of the
separate pointer variables.

The situation with CurrentSnapshot was a bit more
complicated. Usually, it pointed to CurrentSnapshotData, but could
also point to the palloc'd FirstXactSnapshot. This gets rid of the
palloc'd FirstXactSnapshot, and instead we just refrain from modifying
CurrentSnapshotData when in a serializable transaction.
---
 src/backend/utils/time/snapmgr.c | 161 ++++++++++++++-----------------
 1 file changed, 74 insertions(+), 87 deletions(-)

diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index ecddb7f97da..82410a5fff8 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -67,8 +67,8 @@
  * In addition to snapshots pushed to the active snapshot stack, a snapshot
  * can be registered with a resource owner.
  *
- * The FirstXactSnapshot, if any, is treated a bit specially: we increment its
- * regd_count and list it in RegisteredSnapshots, but this reference is not
+ * If FirstXactSnapshotRegistered is set, we increment the static
+ * CurrentSnapshotData's regd_count and list it in RegisteredSnapshots, but this reference is not
  * tracked by a resource owner. We used to use the TopTransactionResourceOwner
  * to track this snapshot reference, but that introduces logical circularity
  * and thus makes it impossible to clean up in a sane fashion.  It's better to
@@ -146,9 +146,6 @@ SnapshotBaseData SnapshotAnyData = {SNAPSHOT_ANY};
 SnapshotBaseData SnapshotToastData = {SNAPSHOT_TOAST};
 
 /* Pointers to valid snapshots */
-static MVCCSnapshot CurrentSnapshot = NULL;
-static MVCCSnapshot SecondarySnapshot = NULL;
-static MVCCSnapshot CatalogSnapshot = NULL;
 static HistoricMVCCSnapshot HistoricSnapshot = NULL;
 
 /*
@@ -193,11 +190,12 @@ static pairingheap RegisteredSnapshots = {&xmin_cmp, NULL, NULL};
 bool		FirstSnapshotSet = false;
 
 /*
- * Remember the serializable transaction snapshot, if any.  We cannot trust
- * FirstSnapshotSet in combination with IsolationUsesXactSnapshot(), because
- * GUC may be reset before us, changing the value of IsolationUsesXactSnapshot.
+ * If set, CurrentSnapshotData is valid and is tracked internally.  We cannot
+ * trust FirstSnapshotSet in combination with IsolationUsesXactSnapshot(),
+ * because GUC may be reset before us, changing the value of
+ * IsolationUsesXactSnapshot.
  */
-static MVCCSnapshot FirstXactSnapshot = NULL;
+static bool FirstXactSnapshotRegistered = false;
 
 /* Define pathname of exported-snapshot files */
 #define SNAPSHOT_EXPORT_DIR "pg_snapshots"
@@ -300,7 +298,7 @@ GetTransactionSnapshot(void)
 		InvalidateCatalogSnapshot();
 
 		Assert(pairingheap_is_empty(&RegisteredSnapshots));
-		Assert(FirstXactSnapshot == NULL);
+		Assert(!FirstXactSnapshotRegistered);
 
 		if (IsInParallelMode())
 			elog(ERROR,
@@ -308,42 +306,44 @@ GetTransactionSnapshot(void)
 
 		/*
 		 * In transaction-snapshot mode, the first snapshot must live until
-		 * end of xact regardless of what the caller does with it, so we must
-		 * make a copy of it rather than returning CurrentSnapshotData
-		 * directly.  Furthermore, if we're running in serializable mode,
-		 * predicate.c needs to wrap the snapshot fetch in its own processing.
+		 * end of xact regardless of what the caller does with it, so we keep
+		 * it in RegisteredSnapshots even though it's not tracked by any
+		 * resource owner.  Furthermore, if we're running in serializable
+		 * mode, predicate.c needs to wrap the snapshot fetch in its own
+		 * processing.
 		 */
 		if (IsolationUsesXactSnapshot())
 		{
 			/* First, create the snapshot in CurrentSnapshotData */
 			if (IsolationIsSerializable())
-				CurrentSnapshot = GetSerializableTransactionSnapshot(&CurrentSnapshotData);
+				GetSerializableTransactionSnapshot(&CurrentSnapshotData);
 			else
-				CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
-
-			/* Make a saved copy */
-			CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
-			FirstXactSnapshot = CurrentSnapshot;
-			/* Mark it as "registered" in FirstXactSnapshot */
-			FirstXactSnapshot->regd_count++;
-			pairingheap_add(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+				GetSnapshotData(&CurrentSnapshotData);
+
+			/* Mark it as "registered" */
+			CurrentSnapshotData.regd_count++;
+			FirstXactSnapshotRegistered = true;
+			pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 		}
 		else
-			CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+			GetSnapshotData(&CurrentSnapshotData);
 
 		FirstSnapshotSet = true;
-		return (Snapshot) CurrentSnapshot;
+		return (Snapshot) &CurrentSnapshotData;
 	}
 
 	if (IsolationUsesXactSnapshot())
-		return (Snapshot) CurrentSnapshot;
+	{
+		Assert(CurrentSnapshotData.valid);
+		return (Snapshot) &CurrentSnapshotData;
+	}
 
 	/* Don't allow catalog snapshot to be older than xact snapshot. */
 	InvalidateCatalogSnapshot();
 
-	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+	GetSnapshotData(&CurrentSnapshotData);
 
-	return (Snapshot) CurrentSnapshot;
+	return (Snapshot) &CurrentSnapshotData;
 }
 
 /*
@@ -372,9 +372,9 @@ GetLatestSnapshot(void)
 	if (!FirstSnapshotSet)
 		return GetTransactionSnapshot();
 
-	SecondarySnapshot = GetSnapshotData(&SecondarySnapshotData);
+	GetSnapshotData(&SecondarySnapshotData);
 
-	return (Snapshot) SecondarySnapshot;
+	return (Snapshot) &SecondarySnapshotData;
 }
 
 /*
@@ -414,15 +414,15 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 	 * scan a relation for which neither catcache nor snapshot invalidations
 	 * are sent, we must refresh the snapshot every time.
 	 */
-	if (CatalogSnapshot &&
+	if (CatalogSnapshotData.valid &&
 		!RelationInvalidatesSnapshotsOnly(relid) &&
 		!RelationHasSysCache(relid))
 		InvalidateCatalogSnapshot();
 
-	if (CatalogSnapshot == NULL)
+	if (!CatalogSnapshotData.valid)
 	{
 		/* Get new snapshot. */
-		CatalogSnapshot = GetSnapshotData(&CatalogSnapshotData);
+		GetSnapshotData(&CatalogSnapshotData);
 
 		/*
 		 * Make sure the catalog snapshot will be accounted for in decisions
@@ -436,10 +436,10 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 		 * NB: it had better be impossible for this to throw error, since the
 		 * CatalogSnapshot pointer is already valid.
 		 */
-		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
+		pairingheap_add(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
 	}
 
-	return (Snapshot) CatalogSnapshot;
+	return (Snapshot) &CatalogSnapshotData;
 }
 
 /*
@@ -455,10 +455,9 @@ GetNonHistoricCatalogSnapshot(Oid relid)
 void
 InvalidateCatalogSnapshot(void)
 {
-	if (CatalogSnapshot)
+	if (CatalogSnapshotData.valid)
 	{
-		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshot->ph_node);
-		CatalogSnapshot = NULL;
+		pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshotData.ph_node);
 		CatalogSnapshotData.valid = false;
 		SnapshotResetXmin();
 		INJECTION_POINT("invalidate-catalog-snapshot-end", NULL);
@@ -478,7 +477,7 @@ InvalidateCatalogSnapshot(void)
 void
 InvalidateCatalogSnapshotConditionally(void)
 {
-	if (CatalogSnapshot &&
+	if (CatalogSnapshotData.valid &&
 		ActiveSnapshot == NULL &&
 		pairingheap_is_singular(&RegisteredSnapshots))
 		InvalidateCatalogSnapshot();
@@ -494,10 +493,10 @@ SnapshotSetCommandId(CommandId curcid)
 	if (!FirstSnapshotSet)
 		return;
 
-	if (CurrentSnapshot)
-		CurrentSnapshot->curcid = curcid;
-	if (SecondarySnapshot)
-		SecondarySnapshot->curcid = curcid;
+	if (CurrentSnapshotData.valid)
+		CurrentSnapshotData.curcid = curcid;
+	if (SecondarySnapshotData.valid)
+		SecondarySnapshotData.curcid = curcid;
 	/* Should we do the same with CatalogSnapshot? */
 }
 
@@ -520,7 +519,7 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	InvalidateCatalogSnapshot();
 
 	Assert(pairingheap_is_empty(&RegisteredSnapshots));
-	Assert(FirstXactSnapshot == NULL);
+	Assert(!FirstXactSnapshotRegistered);
 	Assert(!HistoricSnapshotActive());
 
 	/*
@@ -529,28 +528,28 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	 * CurrentSnapshotData's XID arrays have been allocated, and (2) to update
 	 * the state for GlobalVis*.
 	 */
-	CurrentSnapshot = GetSnapshotData(&CurrentSnapshotData);
+	GetSnapshotData(&CurrentSnapshotData);
 
 	/*
 	 * Now copy appropriate fields from the source snapshot.
 	 */
-	CurrentSnapshot->xmin = sourcesnap->xmin;
-	CurrentSnapshot->xmax = sourcesnap->xmax;
-	CurrentSnapshot->xcnt = sourcesnap->xcnt;
+	CurrentSnapshotData.xmin = sourcesnap->xmin;
+	CurrentSnapshotData.xmax = sourcesnap->xmax;
+	CurrentSnapshotData.xcnt = sourcesnap->xcnt;
 	Assert(sourcesnap->xcnt <= GetMaxSnapshotXidCount());
 	if (sourcesnap->xcnt > 0)
-		memcpy(CurrentSnapshot->xip, sourcesnap->xip,
+		memcpy(CurrentSnapshotData.xip, sourcesnap->xip,
 			   sourcesnap->xcnt * sizeof(TransactionId));
-	CurrentSnapshot->subxcnt = sourcesnap->subxcnt;
+	CurrentSnapshotData.subxcnt = sourcesnap->subxcnt;
 	Assert(sourcesnap->subxcnt <= GetMaxSnapshotSubxidCount());
 	if (sourcesnap->subxcnt > 0)
-		memcpy(CurrentSnapshot->subxip, sourcesnap->subxip,
+		memcpy(CurrentSnapshotData.subxip, sourcesnap->subxip,
 			   sourcesnap->subxcnt * sizeof(TransactionId));
-	CurrentSnapshot->suboverflowed = sourcesnap->suboverflowed;
-	CurrentSnapshot->takenDuringRecovery = sourcesnap->takenDuringRecovery;
+	CurrentSnapshotData.suboverflowed = sourcesnap->suboverflowed;
+	CurrentSnapshotData.takenDuringRecovery = sourcesnap->takenDuringRecovery;
 	/* NB: curcid should NOT be copied, it's a local matter */
 
-	CurrentSnapshot->snapXactCompletionCount = 0;
+	CurrentSnapshotData.snapXactCompletionCount = 0;
 
 	/*
 	 * Now we have to fix what GetSnapshotData did with MyProc->xmin and
@@ -565,13 +564,13 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 	 */
 	if (sourceproc != NULL)
 	{
-		if (!ProcArrayInstallRestoredXmin(CurrentSnapshot->xmin, sourceproc))
+		if (!ProcArrayInstallRestoredXmin(CurrentSnapshotData.xmin, sourceproc))
 			ereport(ERROR,
 					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 					 errmsg("could not import the requested snapshot"),
 					 errdetail("The source transaction is not running anymore.")));
 	}
-	else if (!ProcArrayInstallImportedXmin(CurrentSnapshot->xmin, sourcevxid))
+	else if (!ProcArrayInstallImportedXmin(CurrentSnapshotData.xmin, sourcevxid))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("could not import the requested snapshot"),
@@ -580,20 +579,19 @@ SetTransactionSnapshot(MVCCSnapshot sourcesnap, VirtualTransactionId *sourcevxid
 
 	/*
 	 * In transaction-snapshot mode, the first snapshot must live until end of
-	 * xact, so we must make a copy of it.  Furthermore, if we're running in
-	 * serializable mode, predicate.c needs to do its own processing.
+	 * xact, so we include it in RegisteredSnapshots.  Furthermore, if we're
+	 * running in serializable mode, predicate.c needs to do its own
+	 * processing.
 	 */
 	if (IsolationUsesXactSnapshot())
 	{
 		if (IsolationIsSerializable())
-			SetSerializableTransactionSnapshot(CurrentSnapshot, sourcevxid,
+			SetSerializableTransactionSnapshot(&CurrentSnapshotData, sourcevxid,
 											   sourcepid);
-		/* Make a saved copy */
-		CurrentSnapshot = CopyMVCCSnapshot(CurrentSnapshot);
-		FirstXactSnapshot = CurrentSnapshot;
-		/* Mark it as "registered" in FirstXactSnapshot */
-		FirstXactSnapshot->regd_count++;
-		pairingheap_add(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+		/* Mark it as "registered" */
+		FirstXactSnapshotRegistered = true;
+		CurrentSnapshotData.regd_count++;
+		pairingheap_add(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
 	}
 
 	FirstSnapshotSet = true;
@@ -715,15 +713,7 @@ PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level)
 
 		Assert(origsnap->valid);
 
-		/*
-		 * Checking SecondarySnapshot is probably useless here, but it seems
-		 * better to be sure.
-		 */
-		if (origsnap == CurrentSnapshot || origsnap == SecondarySnapshot ||
-			!origsnap->copied)
-			newsnap = CopyMVCCSnapshot(origsnap);
-		else
-			newsnap = origsnap;
+		newsnap = origsnap->copied ? origsnap : CopyMVCCSnapshot(origsnap);
 		newsnap->active_count++;
 		newactive->as_snap = (Snapshot) newsnap;
 	}
@@ -1035,12 +1025,10 @@ SnapshotResetXmin(void)
 	MVCCSnapshot minSnapshot;
 
 	/*
-	 * These static snapshots are not in the RegisteredSnapshots list, so we
-	 * might advance MyProc->xmin past their xmin. (Note that in case of
-	 * IsolationUsesXactSnapshot() == true, CurrentSnapshot points to the copy
-	 * in FirstSnapshot rather than CurrentSnapshotData.)
+	 * Invalidate these static snapshots so that we can advance xmin.
 	 */
-	CurrentSnapshotData.valid = false;
+	if (!FirstXactSnapshotRegistered)
+		CurrentSnapshotData.valid = false;
 	SecondarySnapshotData.valid = false;
 
 	if (ActiveSnapshot != NULL)
@@ -1144,13 +1132,13 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	 * stacked as active, we don't want the code below to be chasing through a
 	 * dangling pointer.
 	 */
-	if (FirstXactSnapshot != NULL)
+	if (FirstXactSnapshotRegistered)
 	{
-		Assert(FirstXactSnapshot->regd_count > 0);
+		Assert(CurrentSnapshotData.regd_count > 0);
 		Assert(!pairingheap_is_empty(&RegisteredSnapshots));
-		pairingheap_remove(&RegisteredSnapshots, &FirstXactSnapshot->ph_node);
+		pairingheap_remove(&RegisteredSnapshots, &CurrentSnapshotData.ph_node);
+		FirstXactSnapshotRegistered = false;
 	}
-	FirstXactSnapshot = NULL;
 
 	/*
 	 * If we exported any snapshots, clean them up.
@@ -1208,9 +1196,8 @@ AtEOXact_Snapshot(bool isCommit, bool resetXmin)
 	ActiveSnapshot = NULL;
 	pairingheap_reset(&RegisteredSnapshots);
 
-	CurrentSnapshot = NULL;
-	SecondarySnapshot = NULL;
-
+	CurrentSnapshotData.valid = false;
+	SecondarySnapshotData.valid = false;
 	FirstSnapshotSet = false;
 
 	/*
@@ -1771,7 +1758,7 @@ HaveRegisteredOrActiveSnapshot(void)
 	 * removed at any time due to invalidation processing. If explicitly
 	 * registered more than one snapshot has to be in RegisteredSnapshots.
 	 */
-	if (CatalogSnapshot != NULL &&
+	if (CatalogSnapshotData.valid &&
 		pairingheap_is_singular(&RegisteredSnapshots))
 		return false;
 
-- 
2.47.3



^ permalink  raw  reply  [nested|flat] 26+ messages in thread


end of thread, other threads:[~2025-12-22 18:20 UTC | newest]

Thread overview: 26+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-12-06 06:02 [PATCH v10 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v20 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v6 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v8 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v16 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v11 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v5 5/8] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v19 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v9 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v14 2/3] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v4 5/8] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v18 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v10 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v12 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v13 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v7 5/6] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v15 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2021-12-06 06:02 [PATCH v17 2/4] Move removal of old serialized snapshots to custodian. Nathan Bossart <[email protected]>
2025-12-18 23:30 A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
2025-12-19 05:15 ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
2025-12-19 11:40   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
2025-12-19 11:51   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>
2025-12-19 21:32     ` Re: A few patches to clarify snapshot management, part 2 Kirill Reshke <[email protected]>
2025-12-22 02:59     ` Re: A few patches to clarify snapshot management, part 2 Chao Li <[email protected]>
2025-12-19 15:16 ` Re: A few patches to clarify snapshot management, part 2 Andres Freund <[email protected]>
2025-12-22 18:20   ` Re: A few patches to clarify snapshot management, part 2 Heikki Linnakangas <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox