agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v4 2/2] Make resowners more easily extensible.
9+ messages / 3 participants
[nested] [flat]

* [PATCH v4 2/2] Make resowners more easily extensible.
@ 2020-12-16 15:26  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Heikki Linnakangas @ 2020-12-16 15:26 UTC (permalink / raw)

Use a single array and hash, instead of one for each object kind.
---
 src/backend/access/common/tupdesc.c   |   42 +-
 src/backend/jit/jit.c                 |    2 -
 src/backend/jit/llvm/llvmjit.c        |   40 +-
 src/backend/storage/buffer/bufmgr.c   |   45 +-
 src/backend/storage/buffer/localbuf.c |    4 +-
 src/backend/storage/file/fd.c         |   44 +-
 src/backend/storage/ipc/dsm.c         |   44 +-
 src/backend/storage/lmgr/lock.c       |    2 +-
 src/backend/utils/cache/catcache.c    |   74 +-
 src/backend/utils/cache/plancache.c   |   45 +-
 src/backend/utils/cache/relcache.c    |   39 +-
 src/backend/utils/resowner/README     |   19 +-
 src/backend/utils/resowner/resowner.c | 1212 +++++++------------------
 src/backend/utils/time/snapmgr.c      |   38 +-
 src/common/cryptohash_openssl.c       |   45 +-
 src/include/storage/buf_internals.h   |    9 +
 src/include/utils/catcache.h          |    3 -
 src/include/utils/plancache.h         |    2 +
 src/include/utils/resowner.h          |   41 +-
 src/include/utils/resowner_private.h  |  105 ---
 20 files changed, 803 insertions(+), 1052 deletions(-)
 delete mode 100644 src/include/utils/resowner_private.h

diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 902f59440cd..7fa44d5f7ee 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -29,9 +29,21 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/syscache.h"
 
+/* ResourceOwner callbacks to hold tupledesc references  */
+static void ResOwnerReleaseTupleDesc(Datum res);
+static void ResOwnerPrintTupleDescLeakWarning(Datum res);
+
+static ResourceOwnerFuncs tupdesc_resowner_funcs =
+{
+	/* relcache references */
+	.name = "tupdesc reference",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseTupleDesc,
+	.PrintLeakWarning = ResOwnerPrintTupleDescLeakWarning
+};
 
 /*
  * CreateTemplateTupleDesc
@@ -376,9 +388,10 @@ IncrTupleDescRefCount(TupleDesc tupdesc)
 {
 	Assert(tupdesc->tdrefcount >= 0);
 
-	ResourceOwnerEnlargeTupleDescs(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 	tupdesc->tdrefcount++;
-	ResourceOwnerRememberTupleDesc(CurrentResourceOwner, tupdesc);
+	ResourceOwnerRemember(CurrentResourceOwner, PointerGetDatum(tupdesc),
+						  &tupdesc_resowner_funcs);
 }
 
 /*
@@ -394,7 +407,8 @@ DecrTupleDescRefCount(TupleDesc tupdesc)
 {
 	Assert(tupdesc->tdrefcount > 0);
 
-	ResourceOwnerForgetTupleDesc(CurrentResourceOwner, tupdesc);
+	ResourceOwnerForget(CurrentResourceOwner, PointerGetDatum(tupdesc),
+						&tupdesc_resowner_funcs);
 	if (--tupdesc->tdrefcount == 0)
 		FreeTupleDesc(tupdesc);
 }
@@ -925,3 +939,23 @@ BuildDescFromLists(List *names, List *types, List *typmods, List *collations)
 
 	return desc;
 }
+
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseTupleDesc(Datum res)
+{
+	DecrTupleDescRefCount((TupleDesc) DatumGetPointer(res));
+}
+
+static void
+ResOwnerPrintTupleDescLeakWarning(Datum res)
+{
+	TupleDesc tupdesc = (TupleDesc) DatumGetPointer(res);
+
+	elog(WARNING,
+		 "TupleDesc reference leak: TupleDesc %p (%u,%d) still referenced",
+		 tupdesc, tupdesc->tdtypeid, tupdesc->tdtypmod);
+}
diff --git a/src/backend/jit/jit.c b/src/backend/jit/jit.c
index 2da300e000d..1aa04d173b4 100644
--- a/src/backend/jit/jit.c
+++ b/src/backend/jit/jit.c
@@ -26,7 +26,6 @@
 #include "jit/jit.h"
 #include "miscadmin.h"
 #include "utils/fmgrprotos.h"
-#include "utils/resowner_private.h"
 
 /* GUCs */
 bool		jit_enabled = true;
@@ -140,7 +139,6 @@ jit_release_context(JitContext *context)
 	if (provider_successfully_loaded)
 		provider.release_context(context);
 
-	ResourceOwnerForgetJIT(context->resowner, PointerGetDatum(context));
 	pfree(context);
 }
 
diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c
index b0789a5fb80..64c7fd92bc7 100644
--- a/src/backend/jit/llvm/llvmjit.c
+++ b/src/backend/jit/llvm/llvmjit.c
@@ -40,7 +40,7 @@
 #include "portability/instr_time.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 
 /* Handle of a module emitted via ORC JIT */
 typedef struct LLVMJitHandle
@@ -121,8 +121,20 @@ static LLVMOrcLLJITRef llvm_create_jit_instance(LLVMTargetMachineRef tm);
 static char *llvm_error_message(LLVMErrorRef error);
 #endif							/* LLVM_VERSION_MAJOR > 11 */
 
-PG_MODULE_MAGIC;
+/* ResourceOwner callbacks to hold JitContexts  */
+static void ResOwnerReleaseJitContext(Datum res);
+static void ResOwnerPrintJitContextLeakWarning(Datum res);
+
+static ResourceOwnerFuncs jit_funcs =
+{
+	/* relcache references */
+	.name = "LLVM JIT context",
+	.phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+	.ReleaseResource = ResOwnerReleaseJitContext,
+	.PrintLeakWarning = ResOwnerPrintJitContextLeakWarning
+};
 
+PG_MODULE_MAGIC;
 
 /*
  * Initialize LLVM JIT provider.
@@ -151,7 +163,7 @@ llvm_create_context(int jitFlags)
 
 	llvm_session_initialize();
 
-	ResourceOwnerEnlargeJIT(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	context = MemoryContextAllocZero(TopMemoryContext,
 									 sizeof(LLVMJitContext));
@@ -159,7 +171,7 @@ llvm_create_context(int jitFlags)
 
 	/* ensure cleanup */
 	context->base.resowner = CurrentResourceOwner;
-	ResourceOwnerRememberJIT(CurrentResourceOwner, PointerGetDatum(context));
+	ResourceOwnerRemember(CurrentResourceOwner, PointerGetDatum(context), &jit_funcs);
 
 	return context;
 }
@@ -221,6 +233,8 @@ llvm_release_context(JitContext *context)
 
 		pfree(jit_handle);
 	}
+
+	ResourceOwnerForget(context->resowner, PointerGetDatum(context), &jit_funcs);
 }
 
 /*
@@ -1231,3 +1245,21 @@ llvm_error_message(LLVMErrorRef error)
 }
 
 #endif							/* LLVM_VERSION_MAJOR > 11 */
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseJitContext(Datum res)
+{
+	jit_release_context((JitContext *) PointerGetDatum(res));
+}
+
+static void
+ResOwnerPrintJitContextLeakWarning(Datum res)
+{
+	/* XXX: We used to not print these. Was that intentional? */
+	JitContext *context = (JitContext *) PointerGetDatum(res);
+
+	elog(WARNING, "JIT context leak: context %p still referenced", context);
+}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cde869e7d64..82e39de19f9 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -52,7 +52,7 @@
 #include "utils/memdebug.h"
 #include "utils/ps_status.h"
 #include "utils/rel.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/timestamp.h"
 
 
@@ -206,6 +206,18 @@ static PrivateRefCountEntry *GetPrivateRefCountEntry(Buffer buffer, bool do_move
 static inline int32 GetPrivateRefCount(Buffer buffer);
 static void ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref);
 
+/* ResourceOwner callbacks to hold buffer pins */
+static void ResOwnerReleaseBuffer(Datum res);
+static void ResOwnerPrintBufferLeakWarning(Datum res);
+
+ResourceOwnerFuncs buffer_resowner_funcs =
+{
+	.name = "buffer",
+	.phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+	.ReleaseResource = ResOwnerReleaseBuffer,
+	.PrintLeakWarning = ResOwnerPrintBufferLeakWarning
+};
+
 /*
  * Ensure that the PrivateRefCountArray has sufficient space to store one more
  * entry. This has to be called before using NewPrivateRefCountEntry() to fill
@@ -1094,7 +1106,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * pin.
 		 */
 		ReservePrivateRefCountEntry();
-		ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 		/*
 		 * Select a victim buffer.  The buffer is returned with its header
@@ -1604,7 +1616,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy)
 	bool		result;
 	PrivateRefCountEntry *ref;
 
-	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	ref = GetPrivateRefCountEntry(b, true);
 
@@ -1687,7 +1699,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy)
  *
  * As this function is called with the spinlock held, the caller has to
  * previously call ReservePrivateRefCountEntry() and
- * ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+ * ResourceOwnerEnlarge(CurrentResourceOwner);
  *
  * Currently, no callers of this function want to modify the buffer's
  * usage_count at all, so there's no need for a strategy parameter.
@@ -2423,7 +2435,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 
 	/* Make sure we can handle the pin */
 	ReservePrivateRefCountEntry();
-	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	/*
 	 * Check whether buffer needs writing.
@@ -3496,7 +3508,7 @@ FlushRelationBuffers(Relation rel)
 
 		/* Make sure we can handle the pin */
 		ReservePrivateRefCountEntry();
-		ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 		buf_state = LockBufHdr(bufHdr);
 		if (RelFileNodeEquals(bufHdr->tag.rnode, rel->rd_node) &&
@@ -3591,7 +3603,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 
 		/* Make sure we can handle the pin */
 		ReservePrivateRefCountEntry();
-		ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 		buf_state = LockBufHdr(bufHdr);
 		if (RelFileNodeEquals(bufHdr->tag.rnode, srelent->rnode) &&
@@ -3646,7 +3658,7 @@ FlushDatabaseBuffers(Oid dbid)
 
 		/* Make sure we can handle the pin */
 		ReservePrivateRefCountEntry();
-		ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 		buf_state = LockBufHdr(bufHdr);
 		if (bufHdr->tag.rnode.dbNode == dbid &&
@@ -3729,7 +3741,7 @@ void
 IncrBufferRefCount(Buffer buffer)
 {
 	Assert(BufferIsPinned(buffer));
-	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 	if (BufferIsLocal(buffer))
 		LocalRefCount[-buffer - 1]++;
 	else
@@ -4803,3 +4815,18 @@ TestForOldSnapshot_impl(Snapshot snapshot, Relation relation)
 				(errcode(ERRCODE_SNAPSHOT_TOO_OLD),
 				 errmsg("snapshot too old")));
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseBuffer(Datum res)
+{
+	ReleaseBuffer(DatumGetInt32(res));
+}
+
+static void
+ResOwnerPrintBufferLeakWarning(Datum res)
+{
+	PrintBufferLeakWarning(DatumGetInt32(res));
+}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index f7c15ea8a44..3f22d7127b9 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -22,7 +22,7 @@
 #include "storage/bufmgr.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 
 
 /*#define LBDEBUG*/
@@ -124,7 +124,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 		InitLocalBuffers();
 
 	/* Make sure we will have room to remember the buffer pin */
-	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	/* See if the desired buffer already exists */
 	hresult = (LocalBufferLookupEnt *)
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index b58502837aa..1172ea245e0 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -97,7 +97,7 @@
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "utils/guc.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 
 /* Define PG_FLUSH_DATA_WORKS if we have an implementation for pg_flush_data */
 #if defined(HAVE_SYNC_FILE_RANGE)
@@ -340,6 +340,24 @@ static void unlink_if_exists_fname(const char *fname, bool isdir, int elevel);
 static int	fsync_parent_path(const char *fname, int elevel);
 
 
+/* ResourceOwner callbacks to hold virtual file descriptors */
+static void ResOwnerReleaseFile(Datum res);
+static void ResOwnerPrintFileLeakWarning(Datum res);
+
+static ResourceOwnerFuncs file_resowner_funcs =
+{
+	.name = "File",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseFile,
+	.PrintLeakWarning = ResOwnerPrintFileLeakWarning
+};
+
+/* Convenience wrappers over ResourceOwnerRemember/Forget */
+#define ResourceOwnerRememberFile(owner, file) \
+	ResourceOwnerRemember(owner, Int32GetDatum(file), &file_resowner_funcs)
+#define ResourceOwnerForgetFile(owner, file) \
+	ResourceOwnerForget(owner, Int32GetDatum(file), &file_resowner_funcs)
+
 /*
  * pg_fsync --- do fsync with or without writethrough
  */
@@ -1430,7 +1448,7 @@ ReportTemporaryFileUsage(const char *path, off_t size)
 
 /*
  * Called to register a temporary file for automatic close.
- * ResourceOwnerEnlargeFiles(CurrentResourceOwner) must have been called
+ * ResourceOwnerEnlarge(CurrentResourceOwner) must have been called
  * before the file was opened.
  */
 static void
@@ -1612,7 +1630,7 @@ OpenTemporaryFile(bool interXact)
 	 * open it, if we'll be registering it below.
 	 */
 	if (!interXact)
-		ResourceOwnerEnlargeFiles(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	/*
 	 * If some temp tablespace(s) have been given to us, try to use the next
@@ -1742,7 +1760,7 @@ PathNameCreateTemporaryFile(const char *path, bool error_on_failure)
 {
 	File		file;
 
-	ResourceOwnerEnlargeFiles(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	/*
 	 * Open the file.  Note: we don't use O_EXCL, in case there is an orphaned
@@ -1780,7 +1798,7 @@ PathNameOpenTemporaryFile(const char *path, int mode)
 {
 	File		file;
 
-	ResourceOwnerEnlargeFiles(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	file = PathNameOpenFile(path, mode | PG_BINARY);
 
@@ -3700,3 +3718,19 @@ pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, off_t offset)
 
 	return sum;
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseFile(Datum res)
+{
+	FileClose((File) DatumGetInt32(res));
+}
+
+static void
+ResOwnerPrintFileLeakWarning(Datum res)
+{
+	elog(WARNING, "temporary file leak: File %d still referenced",
+		 DatumGetInt32(res));
+}
diff --git a/src/backend/storage/ipc/dsm.c b/src/backend/storage/ipc/dsm.c
index ae82b4bdc0e..31e8860fb53 100644
--- a/src/backend/storage/ipc/dsm.c
+++ b/src/backend/storage/ipc/dsm.c
@@ -37,13 +37,15 @@
 #include "miscadmin.h"
 #include "port/pg_bitutils.h"
 #include "storage/dsm.h"
+#include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 #include "storage/pg_shmem.h"
+#include "storage/shmem.h"
 #include "utils/freepage.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 
 #define PG_DYNSHMEM_CONTROL_MAGIC		0x9a503d32
 
@@ -139,6 +141,25 @@ static dsm_control_header *dsm_control;
 static Size dsm_control_mapped_size = 0;
 static void *dsm_control_impl_private = NULL;
 
+
+/* ResourceOwner callbacks to hold DSM segments */
+static void ResOwnerReleaseDSM(Datum res);
+static void ResOwnerPrintDSMLeakWarning(Datum res);
+
+static ResourceOwnerFuncs dsm_resowner_funcs =
+{
+	.name = "dynamic shared memory segment",
+	.phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+	.ReleaseResource = ResOwnerReleaseDSM,
+	.PrintLeakWarning = ResOwnerPrintDSMLeakWarning
+};
+
+/* Convenience wrappers over ResourceOwnerRemember/Forget */
+#define ResourceOwnerRememberDSM(owner, seg) \
+	ResourceOwnerRemember(owner, PointerGetDatum(seg), &dsm_resowner_funcs)
+#define ResourceOwnerForgetDSM(owner, seg) \
+	ResourceOwnerForget(owner, PointerGetDatum(seg), &dsm_resowner_funcs)
+
 /*
  * Start up the dynamic shared memory system.
  *
@@ -895,7 +916,7 @@ void
 dsm_unpin_mapping(dsm_segment *seg)
 {
 	Assert(seg->resowner == NULL);
-	ResourceOwnerEnlargeDSMs(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 	seg->resowner = CurrentResourceOwner;
 	ResourceOwnerRememberDSM(seg->resowner, seg);
 }
@@ -1162,7 +1183,7 @@ dsm_create_descriptor(void)
 	dsm_segment *seg;
 
 	if (CurrentResourceOwner)
-		ResourceOwnerEnlargeDSMs(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 	seg = MemoryContextAlloc(TopMemoryContext, sizeof(dsm_segment));
 	dlist_push_head(&dsm_segment_list, &seg->node);
@@ -1241,3 +1262,20 @@ is_main_region_dsm_handle(dsm_handle handle)
 {
 	return handle & 1;
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseDSM(Datum res)
+{
+	dsm_detach((dsm_segment *) DatumGetPointer(res));
+}
+static void
+ResOwnerPrintDSMLeakWarning(Datum res)
+{
+	dsm_segment *seg = (dsm_segment *) res;
+
+	elog(WARNING, "dynamic shared memory leak: segment %u still referenced",
+		 dsm_segment_handle(seg));
+}
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 20e50247ea4..de39c844d49 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -47,7 +47,7 @@
 #include "storage/standby.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 
 
 /* This configuration variable is used to set the lock table size */
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 86fdfc454d2..185ff00ee76 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -31,12 +31,13 @@
 #endif
 #include "storage/lmgr.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/syscache.h"
 
 
@@ -104,6 +105,42 @@ static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos,
  *					internal support functions
  */
 
+/* ResourceOwner callbacks to hold catcache references */
+
+static void ResOwnerReleaseCatCache(Datum res);
+static void ResOwnerPrintCatCacheLeakWarning(Datum res);
+static void ResOwnerReleaseCatCacheList(Datum res);
+static void ResOwnerPrintCatCacheListLeakWarning(Datum res);
+
+static ResourceOwnerFuncs catcache_funcs =
+{
+	/* catcache references */
+	.name = "catcache reference",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseCatCache,
+	.PrintLeakWarning = ResOwnerPrintCatCacheLeakWarning
+};
+
+static ResourceOwnerFuncs catlistref_funcs =
+{
+	/* catcache-list pins */
+	.name = "catcache list reference",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseCatCacheList,
+	.PrintLeakWarning = ResOwnerPrintCatCacheListLeakWarning
+};
+
+/* Convenience wrappers over ResourceOwnerRemember/Forget */
+#define ResourceOwnerRememberCatCacheRef(owner, tuple) \
+	ResourceOwnerRemember(owner, PointerGetDatum(tuple), &catcache_funcs)
+#define ResourceOwnerForgetCatCacheRef(owner, tuple) \
+	ResourceOwnerForget(owner, PointerGetDatum(tuple), &catcache_funcs)
+#define ResourceOwnerRememberCatCacheListRef(owner, list) \
+	ResourceOwnerRemember(owner, PointerGetDatum(list), &catlistref_funcs)
+#define ResourceOwnerForgetCatCacheListRef(owner, list) \
+	ResourceOwnerForget(owner, PointerGetDatum(list), &catlistref_funcs)
+
+
 /*
  * Hash and equality functions for system types that are used as cache key
  * fields.  In some cases, we just call the regular SQL-callable functions for
@@ -1270,7 +1307,7 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		if (!ct->negative)
 		{
-			ResourceOwnerEnlargeCatCacheRefs(CurrentResourceOwner);
+			ResourceOwnerEnlarge(CurrentResourceOwner);
 			ct->refcount++;
 			ResourceOwnerRememberCatCacheRef(CurrentResourceOwner, &ct->tuple);
 
@@ -1371,7 +1408,7 @@ SearchCatCacheMiss(CatCache *cache,
 									 hashValue, hashIndex,
 									 false);
 		/* immediately set the refcount to 1 */
-		ResourceOwnerEnlargeCatCacheRefs(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 		ct->refcount++;
 		ResourceOwnerRememberCatCacheRef(CurrentResourceOwner, &ct->tuple);
 		break;					/* assume only one match */
@@ -1583,7 +1620,7 @@ SearchCatCacheList(CatCache *cache,
 		dlist_move_head(&cache->cc_lists, &cl->cache_elem);
 
 		/* Bump the list's refcount and return it */
-		ResourceOwnerEnlargeCatCacheListRefs(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 		cl->refcount++;
 		ResourceOwnerRememberCatCacheListRef(CurrentResourceOwner, cl);
 
@@ -1695,7 +1732,7 @@ SearchCatCacheList(CatCache *cache,
 		table_close(relation, AccessShareLock);
 
 		/* Make sure the resource owner has room to remember this entry. */
-		ResourceOwnerEnlargeCatCacheListRefs(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 
 		/* Now we can build the CatCList entry. */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
@@ -2069,14 +2106,19 @@ PrepareToInvalidateCacheTuple(Relation relation,
 	}
 }
 
-
 /*
- * Subroutines for warning about reference leaks.  These are exported so
- * that resowner.c can call them.
+ * ResourceOwner callbacks
  */
-void
-PrintCatCacheLeakWarning(HeapTuple tuple)
+static void
+ResOwnerReleaseCatCache(Datum res)
+{
+	ReleaseCatCache((HeapTuple) DatumGetPointer(res));
+}
+
+static void
+ResOwnerPrintCatCacheLeakWarning(Datum res)
 {
+	HeapTuple tuple = (HeapTuple) DatumGetPointer(res);
 	CatCTup    *ct = (CatCTup *) (((char *) tuple) -
 								  offsetof(CatCTup, tuple));
 
@@ -2090,9 +2132,17 @@ PrintCatCacheLeakWarning(HeapTuple tuple)
 		 ct->refcount);
 }
 
-void
-PrintCatCacheListLeakWarning(CatCList *list)
+static void
+ResOwnerReleaseCatCacheList(Datum res)
 {
+	ReleaseCatCacheList((CatCList *) DatumGetPointer(res));
+}
+
+static void
+ResOwnerPrintCatCacheListLeakWarning(Datum res)
+{
+	CatCList *list = (CatCList *) DatumGetPointer(res);
+
 	elog(WARNING, "cache reference leak: cache %s (%d), list %p has count %d",
 		 list->my_cache->cc_relname, list->my_cache->id,
 		 list, list->refcount);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index cc04b5b4bef..ecc9296d2d3 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -69,7 +69,7 @@
 #include "tcop/utility.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -115,6 +115,26 @@ static void PlanCacheRelCallback(Datum arg, Oid relid);
 static void PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue);
 static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue);
 
+/* ResourceOwner callbacks to track plancache references */
+static void ResOwnerReleaseCachedPlan(Datum res);
+static void ResOwnerPrintPlanCacheLeakWarning(Datum res);
+
+/* this is exported for ResourceOwnerReleaseAllPlanCacheRefs() */
+ResourceOwnerFuncs planref_resowner_funcs =
+{
+	.name = "plancache reference",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseCachedPlan,
+	.PrintLeakWarning = ResOwnerPrintPlanCacheLeakWarning
+};
+
+/* Convenience wrappers over ResourceOwnerRemember/Forget */
+#define ResourceOwnerRememberPlanCacheRef(owner, plan) \
+	ResourceOwnerRemember(owner, PointerGetDatum(plan), &planref_resowner_funcs)
+#define ResourceOwnerForgetPlanCacheRef(owner, plan) \
+	ResourceOwnerForget(owner, PointerGetDatum(plan), &planref_resowner_funcs)
+
+
 /* GUC parameter */
 int			plan_cache_mode;
 
@@ -1229,7 +1249,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
 
 	/* Flag the plan as in use by caller */
 	if (useResOwner)
-		ResourceOwnerEnlargePlanCacheRefs(CurrentResourceOwner);
+		ResourceOwnerEnlarge(CurrentResourceOwner);
 	plan->refcount++;
 	if (useResOwner)
 		ResourceOwnerRememberPlanCacheRef(CurrentResourceOwner, plan);
@@ -1392,7 +1412,7 @@ CachedPlanAllowsSimpleValidityCheck(CachedPlanSource *plansource,
 	/* Bump refcount if requested. */
 	if (owner)
 	{
-		ResourceOwnerEnlargePlanCacheRefs(owner);
+		ResourceOwnerEnlarge(owner);
 		plan->refcount++;
 		ResourceOwnerRememberPlanCacheRef(owner, plan);
 	}
@@ -1451,7 +1471,7 @@ CachedPlanIsSimplyValid(CachedPlanSource *plansource, CachedPlan *plan,
 	/* It's still good.  Bump refcount if requested. */
 	if (owner)
 	{
-		ResourceOwnerEnlargePlanCacheRefs(owner);
+		ResourceOwnerEnlarge(owner);
 		plan->refcount++;
 		ResourceOwnerRememberPlanCacheRef(owner, plan);
 	}
@@ -2205,3 +2225,20 @@ ResetPlanCache(void)
 		cexpr->is_valid = false;
 	}
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+
+static void
+ResOwnerReleaseCachedPlan(Datum res)
+{
+	ReleaseCachedPlan((CachedPlan *) DatumGetPointer(res), true);
+}
+
+static void
+ResOwnerPrintPlanCacheLeakWarning(Datum res)
+{
+	elog(WARNING, "plancache reference leak: plan %p not closed",
+		 DatumGetPointer(res));
+}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7ef510cd01b..255d19a88b9 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -78,13 +78,14 @@
 #include "storage/smgr.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/relmapper.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
@@ -2078,6 +2079,18 @@ RelationIdGetRelation(Oid relationId)
  * ----------------------------------------------------------------
  */
 
+/* ResourceOwner callbacks to track relcache references */
+static void ResOwnerReleaseRelation(Datum res);
+static void ResOwnerPrintRelCacheLeakWarning(Datum res);
+
+static ResourceOwnerFuncs relref_resowner_funcs =
+{
+	.name = "relcache reference",
+	.phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+	.ReleaseResource = ResOwnerReleaseRelation,
+	.PrintLeakWarning = ResOwnerPrintRelCacheLeakWarning
+};
+
 /*
  * RelationIncrementReferenceCount
  *		Increments relation reference count.
@@ -2089,10 +2102,10 @@ RelationIdGetRelation(Oid relationId)
 void
 RelationIncrementReferenceCount(Relation rel)
 {
-	ResourceOwnerEnlargeRelationRefs(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 	rel->rd_refcnt += 1;
 	if (!IsBootstrapProcessingMode())
-		ResourceOwnerRememberRelationRef(CurrentResourceOwner, rel);
+		ResourceOwnerRemember(CurrentResourceOwner, PointerGetDatum(rel), &relref_resowner_funcs);
 }
 
 /*
@@ -2105,7 +2118,7 @@ RelationDecrementReferenceCount(Relation rel)
 	Assert(rel->rd_refcnt > 0);
 	rel->rd_refcnt -= 1;
 	if (!IsBootstrapProcessingMode())
-		ResourceOwnerForgetRelationRef(CurrentResourceOwner, rel);
+		ResourceOwnerForget(CurrentResourceOwner, PointerGetDatum(rel), &relref_resowner_funcs);
 }
 
 /*
@@ -6417,3 +6430,21 @@ unlink_initfile(const char *initfilename, int elevel)
 							initfilename)));
 	}
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerPrintRelCacheLeakWarning(Datum res)
+{
+	Relation rel = (Relation) res;
+
+	elog(WARNING, "relcache reference leak: relation \"%s\" not closed",
+		 RelationGetRelationName(rel));
+}
+
+static void
+ResOwnerReleaseRelation(Datum res)
+{
+	RelationClose((Relation) res);
+}
diff --git a/src/backend/utils/resowner/README b/src/backend/utils/resowner/README
index 2998f6bb362..34896b303ac 100644
--- a/src/backend/utils/resowner/README
+++ b/src/backend/utils/resowner/README
@@ -60,13 +60,18 @@ subtransaction or portal.  Therefore, the "release" operation on a child
 ResourceOwner transfers lock ownership to the parent instead of actually
 releasing the lock, if isCommit is true.
 
-Currently, ResourceOwners contain direct support for recording ownership of
-buffer pins, lmgr locks, and catcache, relcache, plancache, tupdesc, and
-snapshot references.  Other objects can be associated with a ResourceOwner by
-recording the address of the owning ResourceOwner in such an object.  There is
-an API for other modules to get control during ResourceOwner release, so that
-they can scan their own data structures to find the objects that need to be
-deleted.
+ResourceOwner can record ownership of many different kinds of objects.
+As of this writing, it's used internally for buffer pins, lmgr locks, and
+catcache, relcache, plancache, tupdesc, snapshot, DSM and JIT context
+references. ResourceOwner treats all objects the same, but to register a
+new kind of an object with it, you need to fill in a few callback
+functions, see ResourceOwnerFuncs.
+
+There is also an API for other modules to get control during ResourceOwner
+release, so that they can scan their own data structures to find the objects
+that need to be deleted. This used to be the only way to register new kinds
+of objects with a resource owner; nowadays it easier to write custom
+ResourceOwnerFuncs callbacks.
 
 Whenever we are inside a transaction, the global variable
 CurrentResourceOwner shows which resource owner should be assigned
diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c
index 10f15f6a357..99240679e09 100644
--- a/src/backend/utils/resowner/resowner.c
+++ b/src/backend/utils/resowner/resowner.c
@@ -22,73 +22,44 @@
 
 #include "common/cryptohash.h"
 #include "common/hashfn.h"
-#include "jit/jit.h"
-#include "storage/bufmgr.h"
 #include "storage/ipc.h"
 #include "storage/predicate.h"
 #include "storage/proc.h"
 #include "utils/memutils.h"
-#include "utils/rel.h"
-#include "utils/resowner_private.h"
-#include "utils/snapmgr.h"
-
-
-/*
- * All resource IDs managed by this code are required to fit into a Datum,
- * which is fine since they are generally pointers or integers.
- *
- * Provide Datum conversion macros for a couple of things that are really
- * just "int".
- */
-#define FileGetDatum(file) Int32GetDatum(file)
-#define DatumGetFile(datum) ((File) DatumGetInt32(datum))
-#define BufferGetDatum(buffer) Int32GetDatum(buffer)
-#define DatumGetBuffer(datum) ((Buffer) DatumGetInt32(datum))
+#include "utils/plancache.h"
+#include "utils/resowner.h"
 
 /*
- * ResourceArray is a common structure for storing all types of resource IDs.
- *
- * We manage small sets of resource IDs by keeping them in a simple array:
- * itemsarr[k] holds an ID, for 0 <= k < nitems <= maxitems = capacity.
- *
- * If a set grows large, we switch over to using open-addressing hashing.
- * Then, itemsarr[] is a hash table of "capacity" slots, with each
- * slot holding either an ID or "invalidval".  nitems is the number of valid
- * items present; if it would exceed maxitems, we enlarge the array and
- * re-hash.  In this mode, maxitems should be rather less than capacity so
- * that we don't waste too much time searching for empty slots.
+ * ResourceElem represents a reference associated with a resource owner.
  *
- * In either mode, lastidx remembers the location of the last item inserted
- * or returned by GetAny; this speeds up searches in ResourceArrayRemove.
+ * All objects managed by this code are required to fit into a Datum,
+ * which is fine since they are generally pointers or integers.
  */
-typedef struct ResourceArray
+typedef struct ResourceElem
 {
-	Datum	   *itemsarr;		/* buffer for storing values */
-	Datum		invalidval;		/* value that is considered invalid */
-	uint32		capacity;		/* allocated length of itemsarr[] */
-	uint32		nitems;			/* how many items are stored in items array */
-	uint32		maxitems;		/* current limit on nitems before enlarging */
-	uint32		lastidx;		/* index of last item returned by GetAny */
-} ResourceArray;
+	Datum		item;
+	ResourceOwnerFuncs *kind;
+} ResourceElem;
 
 /*
- * Initially allocated size of a ResourceArray.  Must be power of two since
- * we'll use (arraysize - 1) as mask for hashing.
+ * Size of the small fixed-size array to hold most-recently remembered resources.
  */
-#define RESARRAY_INIT_SIZE 16
+#define RESOWNER_ARRAY_SIZE 8
 
 /*
- * When to switch to hashing vs. simple array logic in a ResourceArray.
+ * Initially allocated size of a ResourceOwner's hash.  Must be power of two since
+ * we'll use (capacity - 1) as mask for hashing.
  */
-#define RESARRAY_MAX_ARRAY 64
-#define RESARRAY_IS_ARRAY(resarr) ((resarr)->capacity <= RESARRAY_MAX_ARRAY)
+#define RESOWNER_HASH_INIT_SIZE 32
 
 /*
- * How many items may be stored in a resource array of given capacity.
+ * How many items may be stored in a hash of given capacity.
  * When this number is reached, we must resize.
  */
-#define RESARRAY_MAX_ITEMS(capacity) \
-	((capacity) <= RESARRAY_MAX_ARRAY ? (capacity) : (capacity)/4 * 3)
+#define RESOWNER_HASH_MAX_ITEMS(capacity) ((capacity)/4 * 3)
+
+StaticAssertDecl(RESOWNER_HASH_MAX_ITEMS(RESOWNER_HASH_INIT_SIZE) > RESOWNER_ARRAY_SIZE,
+				 "initial hash size too small compared to array size");
 
 /*
  * To speed up bulk releasing or reassigning locks from a resource owner to
@@ -118,23 +89,33 @@ typedef struct ResourceOwnerData
 	ResourceOwner nextchild;	/* next child of same parent */
 	const char *name;			/* name (just for debugging) */
 
-	/* We have built-in support for remembering: */
-	ResourceArray bufferarr;	/* owned buffers */
-	ResourceArray catrefarr;	/* catcache references */
-	ResourceArray catlistrefarr;	/* catcache-list pins */
-	ResourceArray relrefarr;	/* relcache references */
-	ResourceArray planrefarr;	/* plancache references */
-	ResourceArray tupdescarr;	/* tupdesc references */
-	ResourceArray snapshotarr;	/* snapshot references */
-	ResourceArray filearr;		/* open temporary files */
-	ResourceArray dsmarr;		/* dynamic shmem segments */
-	ResourceArray jitarr;		/* JIT contexts */
-	ResourceArray cryptohasharr;	/* cryptohash contexts */
+	/*
+	 * These structs keep track of the objects registered with this owner.
+	 *
+	 * We manage a small set of references by keeping them in a simple
+	 * array. When the array gets full, all the elements in the array are
+	 * moved to a hash table. This way, the array always contains a few
+	 * most recently remembered references. To find a particular reference,
+	 * you need to search both the array and the hash table.
+	 */
+	ResourceElem arr[RESOWNER_ARRAY_SIZE];
+	uint32		narr;			/* how many items are stored in the array */
+
+	/*
+	 * The hash table. Uses open-addressing. 'nhash' is the number of items
+	 * present; if it would exceed 'grow_at', we enlarge it and re-hash.
+	 * 'grow_at' should be rather less than 'capacity' so that we don't waste
+	 * too much time searching for empty slots.
+	 */
+	ResourceElem *hash;
+	uint32		nhash;			/* how many items are stored in the hash */
+	uint32		capacity;		/* allocated length of hash[] */
+	uint32		grow_at;		/* grow hash when reach this */
 
 	/* We can remember up to MAX_RESOWNER_LOCKS references to local locks. */
 	int			nlocks;			/* number of owned locks */
 	LOCALLOCK  *locks[MAX_RESOWNER_LOCKS];	/* list of owned locks */
-}			ResourceOwnerData;
+} ResourceOwnerData;
 
 
 /*****************************************************************************
@@ -146,6 +127,18 @@ ResourceOwner CurTransactionResourceOwner = NULL;
 ResourceOwner TopTransactionResourceOwner = NULL;
 ResourceOwner AuxProcessResourceOwner = NULL;
 
+/* #define RESOWNER_STATS */
+/* #define RESOWNER_TRACE */
+
+#ifdef RESOWNER_STATS
+static int narray_lookups = 0;
+static int nhash_lookups = 0;
+#endif
+
+#ifdef RESOWNER_TRACE
+static int resowner_trace_counter = 0;
+#endif
+
 /*
  * List of add-on callbacks for resource releasing
  */
@@ -160,296 +153,311 @@ static ResourceReleaseCallbackItem *ResourceRelease_callbacks = NULL;
 
 
 /* Internal routines */
-static void ResourceArrayInit(ResourceArray *resarr, Datum invalidval);
-static void ResourceArrayEnlarge(ResourceArray *resarr);
-static void ResourceArrayAdd(ResourceArray *resarr, Datum value);
-static bool ResourceArrayRemove(ResourceArray *resarr, Datum value);
-static bool ResourceArrayGetAny(ResourceArray *resarr, Datum *value);
-static void ResourceArrayFree(ResourceArray *resarr);
 static void ResourceOwnerReleaseInternal(ResourceOwner owner,
 										 ResourceReleasePhase phase,
 										 bool isCommit,
 										 bool isTopLevel);
 static void ReleaseAuxProcessResourcesCallback(int code, Datum arg);
-static void PrintRelCacheLeakWarning(Relation rel);
-static void PrintPlanCacheLeakWarning(CachedPlan *plan);
-static void PrintTupleDescLeakWarning(TupleDesc tupdesc);
-static void PrintSnapshotLeakWarning(Snapshot snapshot);
-static void PrintFileLeakWarning(File file);
-static void PrintDSMLeakWarning(dsm_segment *seg);
-static void PrintCryptoHashLeakWarning(Datum handle);
 
 
 /*****************************************************************************
  *	  INTERNAL ROUTINES														 *
  *****************************************************************************/
 
+static inline uint32
+hash_resource_elem(Datum value, ResourceOwnerFuncs *kind)
+{
+	Datum		data[2];
+
+	data[0] = value;
+	data[1] = PointerGetDatum(kind);
+
+	return hash_bytes((unsigned char *) &data, 2 * SIZEOF_DATUM);
+}
 
-/*
- * Initialize a ResourceArray
- */
 static void
-ResourceArrayInit(ResourceArray *resarr, Datum invalidval)
+ResourceArrayAddToHash(ResourceOwner owner, Datum value, ResourceOwnerFuncs *kind)
 {
-	/* Assert it's empty */
-	Assert(resarr->itemsarr == NULL);
-	Assert(resarr->capacity == 0);
-	Assert(resarr->nitems == 0);
-	Assert(resarr->maxitems == 0);
-	/* Remember the appropriate "invalid" value */
-	resarr->invalidval = invalidval;
-	/* We don't allocate any storage until needed */
+	/* Insert into first free slot at or after hash location. */
+	uint32		mask = owner->capacity - 1;
+	uint32		idx;
+
+	idx = hash_resource_elem(value, kind) & mask;
+	for (;;)
+	{
+		if (owner->hash[idx].kind == NULL)
+			break;
+		idx = (idx + 1) & mask;
+	}
+	owner->hash[idx].item = value;
+	owner->hash[idx].kind = kind;
+	owner->nhash++;
 }
 
 /*
- * Make sure there is room for at least one more resource in an array.
- *
- * This is separate from actually inserting a resource because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
+ * Call the ReleaseResource callback on entries with given 'phase'.
  */
 static void
-ResourceArrayEnlarge(ResourceArray *resarr)
+ResourceOwnerReleaseAll(ResourceOwner owner, ResourceReleasePhase phase,
+						bool printLeakWarnings)
 {
-	uint32		i,
-				oldcap,
-				newcap;
-	Datum	   *olditemsarr;
-	Datum	   *newitemsarr;
-
-	if (resarr->nitems < resarr->maxitems)
-		return;					/* no work needed */
+	bool		found;
+	int			capacity;
 
-	olditemsarr = resarr->itemsarr;
-	oldcap = resarr->capacity;
-
-	/* Double the capacity of the array (capacity must stay a power of 2!) */
-	newcap = (oldcap > 0) ? oldcap * 2 : RESARRAY_INIT_SIZE;
-	newitemsarr = (Datum *) MemoryContextAlloc(TopMemoryContext,
-											   newcap * sizeof(Datum));
-	for (i = 0; i < newcap; i++)
-		newitemsarr[i] = resarr->invalidval;
+	/* First handle all the entries in the array. */
+	do
+	{
+		found = false;
+		for (int i = 0; i < owner->narr; i++)
+		{
+			if (owner->arr[i].kind->phase == phase)
+			{
+				Datum		value = owner->arr[i].item;
+				ResourceOwnerFuncs *kind = owner->arr[i].kind;
 
-	/* We assume we can't fail below this point, so OK to scribble on resarr */
-	resarr->itemsarr = newitemsarr;
-	resarr->capacity = newcap;
-	resarr->maxitems = RESARRAY_MAX_ITEMS(newcap);
-	resarr->nitems = 0;
+				if (printLeakWarnings)
+					kind->PrintLeakWarning(value);
+				kind->ReleaseResource(value);
+				found = true;
+			}
+		}
 
-	if (olditemsarr != NULL)
-	{
 		/*
-		 * Transfer any pre-existing entries into the new array; they don't
-		 * necessarily go where they were before, so this simple logic is the
-		 * best way.  Note that if we were managing the set as a simple array,
-		 * the entries after nitems are garbage, but that shouldn't matter
-		 * because we won't get here unless nitems was equal to oldcap.
+		 * If any resources were released, check again because some of the
+		 * elements might have been moved by the callbacks. We don't want to
+		 * miss them.
 		 */
-		for (i = 0; i < oldcap; i++)
+	} while (found && owner->narr > 0);
+
+	/* Ok, the array has now been handled. Then the hash */
+	do
+	{
+		capacity = owner->capacity;
+		for (int idx = 0; idx < capacity; idx++)
 		{
-			if (olditemsarr[i] != resarr->invalidval)
-				ResourceArrayAdd(resarr, olditemsarr[i]);
+			while (owner->hash[idx].kind != NULL &&
+				   owner->hash[idx].kind->phase == phase)
+			{
+				Datum		value = owner->hash[idx].item;
+				ResourceOwnerFuncs *kind = owner->hash[idx].kind;
+
+				if (printLeakWarnings)
+					kind->PrintLeakWarning(value);
+				kind->ReleaseResource(value);
+
+				/*
+				 * If the resource is remembered more than once in this
+				 * resource owner, the ReleaseResource callback might've
+				 * released a different copy of it. Because of that, loop
+				 * to check the same index again.
+				 */
+			}
 		}
 
-		/* And release old array. */
-		pfree(olditemsarr);
-	}
-
-	Assert(resarr->nitems < resarr->maxitems);
+		/*
+		 * It's possible that the callbacks acquired more resources, causing
+		 * the hash table to grow and the existing entries to be moved
+		 * around. If that happened, scan the hash table again, so that we
+		 * don't miss entries that were moved. (XXX: I'm not sure if any of
+		 * the callbacks actually do that, but this is cheap to check, and
+		 * better safe than sorry.)
+		 */
+		Assert(owner->capacity >= capacity);
+	} while (capacity != owner->capacity);
 }
 
+
+/*****************************************************************************
+ *	  EXPORTED ROUTINES														 *
+ *****************************************************************************/
+
+
 /*
- * Add a resource to ResourceArray
+ * ResourceOwnerCreate
+ *		Create an empty ResourceOwner.
  *
- * Caller must have previously done ResourceArrayEnlarge()
+ * All ResourceOwner objects are kept in TopMemoryContext, since they should
+ * only be freed explicitly.
  */
-static void
-ResourceArrayAdd(ResourceArray *resarr, Datum value)
+ResourceOwner
+ResourceOwnerCreate(ResourceOwner parent, const char *name)
 {
-	uint32		idx;
+	ResourceOwner owner;
 
-	Assert(value != resarr->invalidval);
-	Assert(resarr->nitems < resarr->maxitems);
+	owner = (ResourceOwner) MemoryContextAllocZero(TopMemoryContext,
+												   sizeof(ResourceOwnerData));
+	owner->name = name;
 
-	if (RESARRAY_IS_ARRAY(resarr))
+	if (parent)
 	{
-		/* Append to linear array. */
-		idx = resarr->nitems;
+		owner->parent = parent;
+		owner->nextchild = parent->firstchild;
+		parent->firstchild = owner;
 	}
-	else
-	{
-		/* Insert into first free slot at or after hash location. */
-		uint32		mask = resarr->capacity - 1;
 
-		idx = DatumGetUInt32(hash_any((void *) &value, sizeof(value))) & mask;
-		for (;;)
-		{
-			if (resarr->itemsarr[idx] == resarr->invalidval)
-				break;
-			idx = (idx + 1) & mask;
-		}
-	}
-	resarr->lastidx = idx;
-	resarr->itemsarr[idx] = value;
-	resarr->nitems++;
+#ifdef RESOWNER_TRACE
+	elog(LOG, "CREATE %d: %p %s",
+		 resowner_trace_counter++, owner, name);
+#endif
+
+	return owner;
 }
 
 /*
- * Remove a resource from ResourceArray
- *
- * Returns true on success, false if resource was not found.
+ * Make sure there is room for at least one more resource in an array.
  *
- * Note: if same resource ID appears more than once, one instance is removed.
+ * This is separate from actually inserting a resource because if we run out
+ * of memory, it's critical to do so *before* acquiring the resource.
  */
-static bool
-ResourceArrayRemove(ResourceArray *resarr, Datum value)
+void
+ResourceOwnerEnlarge(ResourceOwner owner)
 {
-	uint32		i,
-				idx,
-				lastidx = resarr->lastidx;
-
-	Assert(value != resarr->invalidval);
+	if (owner->narr < RESOWNER_ARRAY_SIZE)
+		return;					/* no work needed */
 
-	/* Search through all items, but try lastidx first. */
-	if (RESARRAY_IS_ARRAY(resarr))
+	/* Is there space in the hash? If not, enlarge it. */
+	if (owner->narr + owner->nhash >= owner->grow_at)
 	{
-		if (lastidx < resarr->nitems &&
-			resarr->itemsarr[lastidx] == value)
-		{
-			resarr->itemsarr[lastidx] = resarr->itemsarr[resarr->nitems - 1];
-			resarr->nitems--;
-			/* Update lastidx to make reverse-order removals fast. */
-			resarr->lastidx = resarr->nitems - 1;
-			return true;
-		}
-		for (i = 0; i < resarr->nitems; i++)
+		uint32		i,
+					oldcap,
+					newcap;
+		ResourceElem *oldhash;
+		ResourceElem *newhash;
+
+		oldhash = owner->hash;
+		oldcap = owner->capacity;
+
+		/* Double the capacity (it must stay a power of 2!) */
+		newcap = (oldcap > 0) ? oldcap * 2 : RESOWNER_HASH_INIT_SIZE;
+		newhash = (ResourceElem *) MemoryContextAllocZero(TopMemoryContext,
+														  newcap * sizeof(ResourceElem));
+
+		/* We assume we can't fail below this point, so OK to scribble on FIXME */
+		owner->hash = newhash;
+		owner->capacity = newcap;
+		owner->grow_at = RESOWNER_HASH_MAX_ITEMS(newcap);
+		owner->nhash = 0;
+
+		if (oldhash != NULL)
 		{
-			if (resarr->itemsarr[i] == value)
+			/*
+			 * Transfer any pre-existing entries into the new hash table; they don't
+			 * necessarily go where they were before, so this simple logic is the
+			 * best way.
+			 */
+			for (i = 0; i < oldcap; i++)
 			{
-				resarr->itemsarr[i] = resarr->itemsarr[resarr->nitems - 1];
-				resarr->nitems--;
-				/* Update lastidx to make reverse-order removals fast. */
-				resarr->lastidx = resarr->nitems - 1;
-				return true;
+				if (oldhash[i].kind != NULL)
+					ResourceArrayAddToHash(owner, oldhash[i].item, oldhash[i].kind);
 			}
+
+			/* And release old hash table. */
+			pfree(oldhash);
 		}
 	}
-	else
-	{
-		uint32		mask = resarr->capacity - 1;
 
-		if (lastidx < resarr->capacity &&
-			resarr->itemsarr[lastidx] == value)
-		{
-			resarr->itemsarr[lastidx] = resarr->invalidval;
-			resarr->nitems--;
-			return true;
-		}
-		idx = DatumGetUInt32(hash_any((void *) &value, sizeof(value))) & mask;
-		for (i = 0; i < resarr->capacity; i++)
-		{
-			if (resarr->itemsarr[idx] == value)
-			{
-				resarr->itemsarr[idx] = resarr->invalidval;
-				resarr->nitems--;
-				return true;
-			}
-			idx = (idx + 1) & mask;
-		}
+	/* Move items from the array to the hash */
+	for (int i = 0; i < owner->narr; i++)
+	{
+		ResourceArrayAddToHash(owner, owner->arr[i].item, owner->arr[i].kind);
 	}
+	owner->narr = 0;
 
-	return false;
+	Assert(owner->nhash < owner->grow_at);
 }
 
 /*
- * Get any convenient entry in a ResourceArray.
+ * Remember that an object is owner by a ReourceOwner
  *
- * "Convenient" is defined as "easy for ResourceArrayRemove to remove";
- * we help that along by setting lastidx to match.  This avoids O(N^2) cost
- * when removing all ResourceArray items during ResourceOwner destruction.
- *
- * Returns true if we found an element, or false if the array is empty.
+ * Caller must have previously done ResourceOwnerEnlarge()
  */
-static bool
-ResourceArrayGetAny(ResourceArray *resarr, Datum *value)
+void
+ResourceOwnerRemember(ResourceOwner owner, Datum value, ResourceOwnerFuncs *kind)
 {
-	if (resarr->nitems == 0)
-		return false;
+	uint32		idx;
 
-	if (RESARRAY_IS_ARRAY(resarr))
-	{
-		/* Linear array: just return the first element. */
-		resarr->lastidx = 0;
-	}
-	else
-	{
-		/* Hash: search forward from wherever we were last. */
-		uint32		mask = resarr->capacity - 1;
+#ifdef RESOWNER_TRACE
+	elog(LOG, "REMEMBER %d: owner %p value " UINT64_FORMAT ", kind: %s",
+		 resowner_trace_counter++, owner, DatumGetUInt64(value), kind->name);
+#endif
 
-		for (;;)
-		{
-			resarr->lastidx &= mask;
-			if (resarr->itemsarr[resarr->lastidx] != resarr->invalidval)
-				break;
-			resarr->lastidx++;
-		}
-	}
+	Assert(owner->narr < RESOWNER_ARRAY_SIZE);
 
-	*value = resarr->itemsarr[resarr->lastidx];
-	return true;
+	/* Append to linear array. */
+	idx = owner->narr;
+	owner->arr[idx].item = value;
+	owner->arr[idx].kind = kind;
+	owner->narr++;
 }
 
 /*
- * Trash a ResourceArray (we don't care about its state after this)
+ * Forget that an object is owned by a ResourceOwner
+ *
+ * Returns true on success. If the resource was not found, returns false,
+ * and calls kind->ForgetError callback.
+ *
+ * Note: if same resource ID is associated with the ResourceOwner more than once,
+ * one instance is removed.
  */
-static void
-ResourceArrayFree(ResourceArray *resarr)
+void
+ResourceOwnerForget(ResourceOwner owner, Datum value, ResourceOwnerFuncs *kind)
 {
-	if (resarr->itemsarr)
-		pfree(resarr->itemsarr);
-}
+	uint32		i,
+				idx;
 
+#ifdef RESOWNER_TRACE
+	elog(LOG, "FORGET %d: owner %p value " UINT64_FORMAT ", kind: %s",
+		 resowner_trace_counter++, owner, DatumGetUInt64(value), kind->name);
+#endif
 
-/*****************************************************************************
- *	  EXPORTED ROUTINES														 *
- *****************************************************************************/
-
+	/* Search through all items, but check the array first. */
+	for (i = 0; i < owner->narr; i++)
+	{
+		if (owner->arr[i].item == value &&
+			owner->arr[i].kind == kind)
+		{
+			owner->arr[i] = owner->arr[owner->narr - 1];
+			owner->narr--;
 
-/*
- * ResourceOwnerCreate
- *		Create an empty ResourceOwner.
- *
- * All ResourceOwner objects are kept in TopMemoryContext, since they should
- * only be freed explicitly.
- */
-ResourceOwner
-ResourceOwnerCreate(ResourceOwner parent, const char *name)
-{
-	ResourceOwner owner;
+#ifdef RESOWNER_STATS
+			narray_lookups++;
+#endif
 
-	owner = (ResourceOwner) MemoryContextAllocZero(TopMemoryContext,
-												   sizeof(ResourceOwnerData));
-	owner->name = name;
+			return;
+		}
+	}
 
-	if (parent)
+	/* Search hash */
+	if (owner->nhash > 0)
 	{
-		owner->parent = parent;
-		owner->nextchild = parent->firstchild;
-		parent->firstchild = owner;
-	}
+		uint32		mask = owner->capacity - 1;
 
-	ResourceArrayInit(&(owner->bufferarr), BufferGetDatum(InvalidBuffer));
-	ResourceArrayInit(&(owner->catrefarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->catlistrefarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->relrefarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->planrefarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->tupdescarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->snapshotarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->filearr), FileGetDatum(-1));
-	ResourceArrayInit(&(owner->dsmarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->jitarr), PointerGetDatum(NULL));
-	ResourceArrayInit(&(owner->cryptohasharr), PointerGetDatum(NULL));
+		idx = hash_resource_elem(value, kind) & mask;
+		for (i = 0; i < owner->capacity; i++)
+		{
+			if (owner->hash[idx].item == value &&
+				owner->hash[idx].kind == kind)
+			{
+				owner->hash[idx].item = (Datum) 0;
+				owner->hash[idx].kind = NULL;
+				owner->nhash--;
+
+#ifdef RESOWNER_STATS
+				nhash_lookups++;
+#endif
+				return;
+			}
+			idx = (idx + 1) & mask;
+		}
+	}
 
-	return owner;
+	/*
+	 * Use %p to print the reference, since most objects tracked by a resource owner
+	 * are pointers. It's a bit misleading if it's not a pointer, but this is a
+	 * programmer error, anyway.
+	 */
+	elog(ERROR, "%s %p is not owned by resource owner %s",
+		 kind->name, DatumGetPointer(value), owner->name);
 }
 
 /*
@@ -486,6 +494,15 @@ ResourceOwnerRelease(ResourceOwner owner,
 {
 	/* There's not currently any setup needed before recursing */
 	ResourceOwnerReleaseInternal(owner, phase, isCommit, isTopLevel);
+
+#ifdef RESOWNER_STATS
+	if (isTopLevel)
+	{
+		elog(LOG, "RESOWNER STATS: lookups: array %d, hash %d", narray_lookups, nhash_lookups);
+		narray_lookups = 0;
+		nhash_lookups = 0;
+	}
+#endif
 }
 
 static void
@@ -497,7 +514,6 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
 	ResourceOwner child;
 	ResourceOwner save;
 	ResourceReleaseCallbackItem *item;
-	Datum		foundres;
 
 	/* Recurse to handle descendants */
 	for (child = owner->firstchild; child != NULL; child = child->nextchild)
@@ -513,61 +529,13 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
 	if (phase == RESOURCE_RELEASE_BEFORE_LOCKS)
 	{
 		/*
-		 * Release buffer pins.  Note that ReleaseBuffer will remove the
-		 * buffer entry from our array, so we just have to iterate till there
-		 * are none.
+		 * Release all references that need to be released before the locks.
 		 *
-		 * During a commit, there shouldn't be any remaining pins --- that
+		 * During a commit, there shouldn't be any remaining references --- that
 		 * would indicate failure to clean up the executor correctly --- so
 		 * issue warnings.  In the abort case, just clean up quietly.
 		 */
-		while (ResourceArrayGetAny(&(owner->bufferarr), &foundres))
-		{
-			Buffer		res = DatumGetBuffer(foundres);
-
-			if (isCommit)
-				PrintBufferLeakWarning(res);
-			ReleaseBuffer(res);
-		}
-
-		/* Ditto for relcache references */
-		while (ResourceArrayGetAny(&(owner->relrefarr), &foundres))
-		{
-			Relation	res = (Relation) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintRelCacheLeakWarning(res);
-			RelationClose(res);
-		}
-
-		/* Ditto for dynamic shared memory segments */
-		while (ResourceArrayGetAny(&(owner->dsmarr), &foundres))
-		{
-			dsm_segment *res = (dsm_segment *) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintDSMLeakWarning(res);
-			dsm_detach(res);
-		}
-
-		/* Ditto for JIT contexts */
-		while (ResourceArrayGetAny(&(owner->jitarr), &foundres))
-		{
-			JitContext *context = (JitContext *) PointerGetDatum(foundres);
-
-			jit_release_context(context);
-		}
-
-		/* Ditto for cryptohash contexts */
-		while (ResourceArrayGetAny(&(owner->cryptohasharr), &foundres))
-		{
-			pg_cryptohash_ctx *context =
-			(pg_cryptohash_ctx *) PointerGetDatum(foundres);
-
-			if (isCommit)
-				PrintCryptoHashLeakWarning(foundres);
-			pg_cryptohash_free(context);
-		}
+		ResourceOwnerReleaseAll(owner, phase, isCommit);
 	}
 	else if (phase == RESOURCE_RELEASE_LOCKS)
 	{
@@ -576,7 +544,7 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
 			/*
 			 * For a top-level xact we are going to release all locks (or at
 			 * least all non-session locks), so just do a single lmgr call at
-			 * the top of the recursion.
+			 * the top of the recursion
 			 */
 			if (owner == TopTransactionResourceOwner)
 			{
@@ -620,70 +588,9 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
 	else if (phase == RESOURCE_RELEASE_AFTER_LOCKS)
 	{
 		/*
-		 * Release catcache references.  Note that ReleaseCatCache will remove
-		 * the catref entry from our array, so we just have to iterate till
-		 * there are none.
-		 *
-		 * As with buffer pins, warn if any are left at commit time.
+		 * Release all references that need to be released after the locks.
 		 */
-		while (ResourceArrayGetAny(&(owner->catrefarr), &foundres))
-		{
-			HeapTuple	res = (HeapTuple) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintCatCacheLeakWarning(res);
-			ReleaseCatCache(res);
-		}
-
-		/* Ditto for catcache lists */
-		while (ResourceArrayGetAny(&(owner->catlistrefarr), &foundres))
-		{
-			CatCList   *res = (CatCList *) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintCatCacheListLeakWarning(res);
-			ReleaseCatCacheList(res);
-		}
-
-		/* Ditto for plancache references */
-		while (ResourceArrayGetAny(&(owner->planrefarr), &foundres))
-		{
-			CachedPlan *res = (CachedPlan *) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintPlanCacheLeakWarning(res);
-			ReleaseCachedPlan(res, true);
-		}
-
-		/* Ditto for tupdesc references */
-		while (ResourceArrayGetAny(&(owner->tupdescarr), &foundres))
-		{
-			TupleDesc	res = (TupleDesc) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintTupleDescLeakWarning(res);
-			DecrTupleDescRefCount(res);
-		}
-
-		/* Ditto for snapshot references */
-		while (ResourceArrayGetAny(&(owner->snapshotarr), &foundres))
-		{
-			Snapshot	res = (Snapshot) DatumGetPointer(foundres);
-
-			if (isCommit)
-				PrintSnapshotLeakWarning(res);
-			UnregisterSnapshot(res);
-		}
-
-		/* Ditto for temporary files */
-		while (ResourceArrayGetAny(&(owner->filearr), &foundres))
-		{
-			File		res = DatumGetFile(foundres);
-
-			if (isCommit)
-				PrintFileLeakWarning(res);
-			FileClose(res);
-		}
+		ResourceOwnerReleaseAll(owner, phase, isCommit);
 	}
 
 	/* Let add-on modules get a chance too */
@@ -704,16 +611,42 @@ void
 ResourceOwnerReleaseAllPlanCacheRefs(ResourceOwner owner)
 {
 	ResourceOwner save;
-	Datum		foundres;
 
 	save = CurrentResourceOwner;
 	CurrentResourceOwner = owner;
-	while (ResourceArrayGetAny(&(owner->planrefarr), &foundres))
+
+	/* array first */
+	for (int i = 0; i < owner->narr; i++)
+	{
+		if (owner->arr[i].kind == &planref_resowner_funcs)
+		{
+			CachedPlan *planref = (CachedPlan *) DatumGetPointer(owner->arr[i].item);
+
+			owner->arr[i] = owner->arr[owner->narr - 1];
+			owner->narr--;
+			i--;
+
+			/* pass 'false' because we already removed the entry from the resowner */
+			ReleaseCachedPlan(planref, false);
+		}
+	}
+
+	/* Then hash */
+	for (int i = 0; i < owner->capacity; i++)
 	{
-		CachedPlan *res = (CachedPlan *) DatumGetPointer(foundres);
+		if (owner->hash[i].kind == &planref_resowner_funcs)
+		{
+			CachedPlan *planref = (CachedPlan *) DatumGetPointer(owner->hash[i].item);
+
+			owner->hash[i].item = (Datum) 0;
+			owner->hash[i].kind = NULL;
+			owner->nhash--;
 
-		ReleaseCachedPlan(res, true);
+			/* pass 'false' because we already removed the entry from the resowner */
+			ReleaseCachedPlan(planref, false);
+		}
 	}
+
 	CurrentResourceOwner = save;
 }
 
@@ -730,19 +663,15 @@ ResourceOwnerDelete(ResourceOwner owner)
 	Assert(owner != CurrentResourceOwner);
 
 	/* And it better not own any resources, either */
-	Assert(owner->bufferarr.nitems == 0);
-	Assert(owner->catrefarr.nitems == 0);
-	Assert(owner->catlistrefarr.nitems == 0);
-	Assert(owner->relrefarr.nitems == 0);
-	Assert(owner->planrefarr.nitems == 0);
-	Assert(owner->tupdescarr.nitems == 0);
-	Assert(owner->snapshotarr.nitems == 0);
-	Assert(owner->filearr.nitems == 0);
-	Assert(owner->dsmarr.nitems == 0);
-	Assert(owner->jitarr.nitems == 0);
-	Assert(owner->cryptohasharr.nitems == 0);
+	Assert(owner->narr == 0);
+	Assert(owner->nhash == 0);
 	Assert(owner->nlocks == 0 || owner->nlocks == MAX_RESOWNER_LOCKS + 1);
 
+#ifdef RESOWNER_TRACE
+	elog(LOG, "DELETE %d: %p %s",
+		 resowner_trace_counter++, owner, owner->name);
+#endif
+
 	/*
 	 * Delete children.  The recursive call will delink the child from me, so
 	 * just iterate as long as there is a child.
@@ -758,18 +687,8 @@ ResourceOwnerDelete(ResourceOwner owner)
 	ResourceOwnerNewParent(owner, NULL);
 
 	/* And free the object. */
-	ResourceArrayFree(&(owner->bufferarr));
-	ResourceArrayFree(&(owner->catrefarr));
-	ResourceArrayFree(&(owner->catlistrefarr));
-	ResourceArrayFree(&(owner->relrefarr));
-	ResourceArrayFree(&(owner->planrefarr));
-	ResourceArrayFree(&(owner->tupdescarr));
-	ResourceArrayFree(&(owner->snapshotarr));
-	ResourceArrayFree(&(owner->filearr));
-	ResourceArrayFree(&(owner->dsmarr));
-	ResourceArrayFree(&(owner->jitarr));
-	ResourceArrayFree(&(owner->cryptohasharr));
-
+	if (owner->hash)
+		pfree(owner->hash);
 	pfree(owner);
 }
 
@@ -922,44 +841,6 @@ ReleaseAuxProcessResourcesCallback(int code, Datum arg)
 	ReleaseAuxProcessResources(isCommit);
 }
 
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * buffer array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeBuffers(ResourceOwner owner)
-{
-	/* We used to allow pinning buffers without a resowner, but no more */
-	Assert(owner != NULL);
-	ResourceArrayEnlarge(&(owner->bufferarr));
-}
-
-/*
- * Remember that a buffer pin is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeBuffers()
- */
-void
-ResourceOwnerRememberBuffer(ResourceOwner owner, Buffer buffer)
-{
-	ResourceArrayAdd(&(owner->bufferarr), BufferGetDatum(buffer));
-}
-
-/*
- * Forget that a buffer pin is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetBuffer(ResourceOwner owner, Buffer buffer)
-{
-	if (!ResourceArrayRemove(&(owner->bufferarr), BufferGetDatum(buffer)))
-		elog(ERROR, "buffer %d is not owned by resource owner %s",
-			 buffer, owner->name);
-}
-
 /*
  * Remember that a Local Lock is owned by a ResourceOwner
  *
@@ -1011,424 +892,3 @@ ResourceOwnerForgetLock(ResourceOwner owner, LOCALLOCK *locallock)
 	elog(ERROR, "lock reference %p is not owned by resource owner %s",
 		 locallock, owner->name);
 }
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * catcache reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeCatCacheRefs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->catrefarr));
-}
-
-/*
- * Remember that a catcache reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeCatCacheRefs()
- */
-void
-ResourceOwnerRememberCatCacheRef(ResourceOwner owner, HeapTuple tuple)
-{
-	ResourceArrayAdd(&(owner->catrefarr), PointerGetDatum(tuple));
-}
-
-/*
- * Forget that a catcache reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetCatCacheRef(ResourceOwner owner, HeapTuple tuple)
-{
-	if (!ResourceArrayRemove(&(owner->catrefarr), PointerGetDatum(tuple)))
-		elog(ERROR, "catcache reference %p is not owned by resource owner %s",
-			 tuple, owner->name);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * catcache-list reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeCatCacheListRefs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->catlistrefarr));
-}
-
-/*
- * Remember that a catcache-list reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeCatCacheListRefs()
- */
-void
-ResourceOwnerRememberCatCacheListRef(ResourceOwner owner, CatCList *list)
-{
-	ResourceArrayAdd(&(owner->catlistrefarr), PointerGetDatum(list));
-}
-
-/*
- * Forget that a catcache-list reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetCatCacheListRef(ResourceOwner owner, CatCList *list)
-{
-	if (!ResourceArrayRemove(&(owner->catlistrefarr), PointerGetDatum(list)))
-		elog(ERROR, "catcache list reference %p is not owned by resource owner %s",
-			 list, owner->name);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * relcache reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeRelationRefs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->relrefarr));
-}
-
-/*
- * Remember that a relcache reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeRelationRefs()
- */
-void
-ResourceOwnerRememberRelationRef(ResourceOwner owner, Relation rel)
-{
-	ResourceArrayAdd(&(owner->relrefarr), PointerGetDatum(rel));
-}
-
-/*
- * Forget that a relcache reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetRelationRef(ResourceOwner owner, Relation rel)
-{
-	if (!ResourceArrayRemove(&(owner->relrefarr), PointerGetDatum(rel)))
-		elog(ERROR, "relcache reference %s is not owned by resource owner %s",
-			 RelationGetRelationName(rel), owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintRelCacheLeakWarning(Relation rel)
-{
-	elog(WARNING, "relcache reference leak: relation \"%s\" not closed",
-		 RelationGetRelationName(rel));
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * plancache reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargePlanCacheRefs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->planrefarr));
-}
-
-/*
- * Remember that a plancache reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargePlanCacheRefs()
- */
-void
-ResourceOwnerRememberPlanCacheRef(ResourceOwner owner, CachedPlan *plan)
-{
-	ResourceArrayAdd(&(owner->planrefarr), PointerGetDatum(plan));
-}
-
-/*
- * Forget that a plancache reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetPlanCacheRef(ResourceOwner owner, CachedPlan *plan)
-{
-	if (!ResourceArrayRemove(&(owner->planrefarr), PointerGetDatum(plan)))
-		elog(ERROR, "plancache reference %p is not owned by resource owner %s",
-			 plan, owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintPlanCacheLeakWarning(CachedPlan *plan)
-{
-	elog(WARNING, "plancache reference leak: plan %p not closed", plan);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * tupdesc reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeTupleDescs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->tupdescarr));
-}
-
-/*
- * Remember that a tupdesc reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeTupleDescs()
- */
-void
-ResourceOwnerRememberTupleDesc(ResourceOwner owner, TupleDesc tupdesc)
-{
-	ResourceArrayAdd(&(owner->tupdescarr), PointerGetDatum(tupdesc));
-}
-
-/*
- * Forget that a tupdesc reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetTupleDesc(ResourceOwner owner, TupleDesc tupdesc)
-{
-	if (!ResourceArrayRemove(&(owner->tupdescarr), PointerGetDatum(tupdesc)))
-		elog(ERROR, "tupdesc reference %p is not owned by resource owner %s",
-			 tupdesc, owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintTupleDescLeakWarning(TupleDesc tupdesc)
-{
-	elog(WARNING,
-		 "TupleDesc reference leak: TupleDesc %p (%u,%d) still referenced",
-		 tupdesc, tupdesc->tdtypeid, tupdesc->tdtypmod);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * snapshot reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeSnapshots(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->snapshotarr));
-}
-
-/*
- * Remember that a snapshot reference is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeSnapshots()
- */
-void
-ResourceOwnerRememberSnapshot(ResourceOwner owner, Snapshot snapshot)
-{
-	ResourceArrayAdd(&(owner->snapshotarr), PointerGetDatum(snapshot));
-}
-
-/*
- * Forget that a snapshot reference is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetSnapshot(ResourceOwner owner, Snapshot snapshot)
-{
-	if (!ResourceArrayRemove(&(owner->snapshotarr), PointerGetDatum(snapshot)))
-		elog(ERROR, "snapshot reference %p is not owned by resource owner %s",
-			 snapshot, owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintSnapshotLeakWarning(Snapshot snapshot)
-{
-	elog(WARNING, "Snapshot reference leak: Snapshot %p still referenced",
-		 snapshot);
-}
-
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * files reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeFiles(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->filearr));
-}
-
-/*
- * Remember that a temporary file is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeFiles()
- */
-void
-ResourceOwnerRememberFile(ResourceOwner owner, File file)
-{
-	ResourceArrayAdd(&(owner->filearr), FileGetDatum(file));
-}
-
-/*
- * Forget that a temporary file is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetFile(ResourceOwner owner, File file)
-{
-	if (!ResourceArrayRemove(&(owner->filearr), FileGetDatum(file)))
-		elog(ERROR, "temporary file %d is not owned by resource owner %s",
-			 file, owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintFileLeakWarning(File file)
-{
-	elog(WARNING, "temporary file leak: File %d still referenced",
-		 file);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * dynamic shmem segment reference array.
- *
- * This is separate from actually inserting an entry because if we run out
- * of memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeDSMs(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->dsmarr));
-}
-
-/*
- * Remember that a dynamic shmem segment is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeDSMs()
- */
-void
-ResourceOwnerRememberDSM(ResourceOwner owner, dsm_segment *seg)
-{
-	ResourceArrayAdd(&(owner->dsmarr), PointerGetDatum(seg));
-}
-
-/*
- * Forget that a dynamic shmem segment is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetDSM(ResourceOwner owner, dsm_segment *seg)
-{
-	if (!ResourceArrayRemove(&(owner->dsmarr), PointerGetDatum(seg)))
-		elog(ERROR, "dynamic shared memory segment %u is not owned by resource owner %s",
-			 dsm_segment_handle(seg), owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintDSMLeakWarning(dsm_segment *seg)
-{
-	elog(WARNING, "dynamic shared memory leak: segment %u still referenced",
-		 dsm_segment_handle(seg));
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * JIT context reference array.
- *
- * This is separate from actually inserting an entry because if we run out of
- * memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeJIT(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->jitarr));
-}
-
-/*
- * Remember that a JIT context is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeJIT()
- */
-void
-ResourceOwnerRememberJIT(ResourceOwner owner, Datum handle)
-{
-	ResourceArrayAdd(&(owner->jitarr), handle);
-}
-
-/*
- * Forget that a JIT context is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetJIT(ResourceOwner owner, Datum handle)
-{
-	if (!ResourceArrayRemove(&(owner->jitarr), handle))
-		elog(ERROR, "JIT context %p is not owned by resource owner %s",
-			 DatumGetPointer(handle), owner->name);
-}
-
-/*
- * Make sure there is room for at least one more entry in a ResourceOwner's
- * cryptohash context reference array.
- *
- * This is separate from actually inserting an entry because if we run out of
- * memory, it's critical to do so *before* acquiring the resource.
- */
-void
-ResourceOwnerEnlargeCryptoHash(ResourceOwner owner)
-{
-	ResourceArrayEnlarge(&(owner->cryptohasharr));
-}
-
-/*
- * Remember that a cryptohash context is owned by a ResourceOwner
- *
- * Caller must have previously done ResourceOwnerEnlargeCryptoHash()
- */
-void
-ResourceOwnerRememberCryptoHash(ResourceOwner owner, Datum handle)
-{
-	ResourceArrayAdd(&(owner->cryptohasharr), handle);
-}
-
-/*
- * Forget that a cryptohash context is owned by a ResourceOwner
- */
-void
-ResourceOwnerForgetCryptoHash(ResourceOwner owner, Datum handle)
-{
-	if (!ResourceArrayRemove(&(owner->cryptohasharr), handle))
-		elog(ERROR, "cryptohash context %p is not owned by resource owner %s",
-			 DatumGetPointer(handle), owner->name);
-}
-
-/*
- * Debugging subroutine
- */
-static void
-PrintCryptoHashLeakWarning(Datum handle)
-{
-	elog(WARNING, "cryptohash context reference leak: context %p still referenced",
-		 DatumGetPointer(handle));
-}
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index ae16c3ed7d6..10cec8d39d0 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -66,7 +66,7 @@
 #include "utils/memutils.h"
 #include "utils/old_snapshot.h"
 #include "utils/rel.h"
-#include "utils/resowner_private.h"
+#include "utils/resowner.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -174,6 +174,18 @@ static Snapshot CopySnapshot(Snapshot snapshot);
 static void FreeSnapshot(Snapshot snapshot);
 static void SnapshotResetXmin(void);
 
+/* ResourceOwner callbacks to track snapshot references */
+static void ResOwnerReleaseSnapshot(Datum res);
+static void ResOwnerPrintSnapshotLeakWarning(Datum res);
+
+static ResourceOwnerFuncs snapshot_resowner_funcs =
+{
+	.name = "snapshot reference",
+	.phase = RESOURCE_RELEASE_AFTER_LOCKS,
+	.ReleaseResource = ResOwnerReleaseSnapshot,
+	.PrintLeakWarning = ResOwnerPrintSnapshotLeakWarning
+};
+
 /*
  * Snapshot fields to be serialized.
  *
@@ -831,9 +843,10 @@ RegisterSnapshotOnOwner(Snapshot snapshot, ResourceOwner owner)
 	snap = snapshot->copied ? snapshot : CopySnapshot(snapshot);
 
 	/* and tell resowner.c about it */
-	ResourceOwnerEnlargeSnapshots(owner);
+	ResourceOwnerEnlarge(owner);
 	snap->regd_count++;
-	ResourceOwnerRememberSnapshot(owner, snap);
+	ResourceOwnerRemember(owner, PointerGetDatum(snap),
+						  &snapshot_resowner_funcs);
 
 	if (snap->regd_count == 1)
 		pairingheap_add(&RegisteredSnapshots, &snap->ph_node);
@@ -870,7 +883,8 @@ UnregisterSnapshotFromOwner(Snapshot snapshot, ResourceOwner owner)
 	Assert(snapshot->regd_count > 0);
 	Assert(!pairingheap_is_empty(&RegisteredSnapshots));
 
-	ResourceOwnerForgetSnapshot(owner, snapshot);
+	ResourceOwnerForget(owner, PointerGetDatum(snapshot),
+						&snapshot_resowner_funcs);
 
 	snapshot->regd_count--;
 	if (snapshot->regd_count == 0)
@@ -2345,3 +2359,19 @@ XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot)
 
 	return false;
 }
+
+/*
+ * ResourceOwner callbacks
+ */
+static void
+ResOwnerReleaseSnapshot(Datum res)
+{
+	UnregisterSnapshot((Snapshot) DatumGetPointer(res));
+}
+
+static void
+ResOwnerPrintSnapshotLeakWarning(Datum res)
+{
+	elog(WARNING, "Snapshot reference leak: Snapshot %p still referenced",
+		 DatumGetPointer(res));
+}
diff --git a/src/common/cryptohash_openssl.c b/src/common/cryptohash_openssl.c
index 551ec392b60..642e72d8c0f 100644
--- a/src/common/cryptohash_openssl.c
+++ b/src/common/cryptohash_openssl.c
@@ -27,7 +27,6 @@
 #ifndef FRONTEND
 #include "utils/memutils.h"
 #include "utils/resowner.h"
-#include "utils/resowner_private.h"
 #endif
 
 /*
@@ -60,6 +59,21 @@ struct pg_cryptohash_ctx
 #endif
 };
 
+/* ResourceOwner callbacks to hold JitContexts  */
+#ifndef FRONTEND
+static void ResOwnerReleaseCryptoHash(Datum res);
+static void ResOwnerPrintCryptoHashLeakWarning(Datum res);
+
+static ResourceOwnerFuncs cryptohash_funcs =
+{
+	/* relcache references */
+	.name = "OpenSSL cryptohash context",
+	.phase = RESOURCE_RELEASE_BEFORE_LOCKS,
+	.ReleaseResource = ResOwnerReleaseCryptoHash,
+	.PrintLeakWarning = ResOwnerPrintCryptoHashLeakWarning,
+};
+#endif
+
 /*
  * pg_cryptohash_create
  *
@@ -77,7 +91,7 @@ pg_cryptohash_create(pg_cryptohash_type type)
 	 * allocation to avoid leaking.
 	 */
 #ifndef FRONTEND
-	ResourceOwnerEnlargeCryptoHash(CurrentResourceOwner);
+	ResourceOwnerEnlarge(CurrentResourceOwner);
 #endif
 
 	ctx = ALLOC(sizeof(pg_cryptohash_ctx));
@@ -106,8 +120,8 @@ pg_cryptohash_create(pg_cryptohash_type type)
 
 #ifndef FRONTEND
 	ctx->resowner = CurrentResourceOwner;
-	ResourceOwnerRememberCryptoHash(CurrentResourceOwner,
-									PointerGetDatum(ctx));
+	ResourceOwnerRemember(CurrentResourceOwner, PointerGetDatum(ctx),
+						  &cryptohash_funcs);
 #endif
 
 	return ctx;
@@ -207,10 +221,29 @@ pg_cryptohash_free(pg_cryptohash_ctx *ctx)
 	EVP_MD_CTX_destroy(ctx->evpctx);
 
 #ifndef FRONTEND
-	ResourceOwnerForgetCryptoHash(ctx->resowner,
-								  PointerGetDatum(ctx));
+	ResourceOwnerForget(ctx->resowner, PointerGetDatum(ctx),
+						&cryptohash_funcs);
 #endif
 
 	explicit_bzero(ctx, sizeof(pg_cryptohash_ctx));
 	FREE(ctx);
 }
+
+
+/*
+ * ResourceOwner callbacks
+ */
+#ifndef FRONTEND
+static void
+ResOwnerReleaseCryptoHash(Datum res)
+{
+	pg_cryptohash_free((pg_cryptohash_ctx *) DatumGetPointer(res));
+}
+
+static void
+ResOwnerPrintCryptoHashLeakWarning(Datum res)
+{
+	elog(WARNING, "cryptohash context reference leak: context %p still referenced",
+		 DatumGetPointer(res));
+}
+#endif
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index f6b57829653..6815dc38a16 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -296,6 +296,15 @@ typedef struct CkptSortItem
 
 extern CkptSortItem *CkptBufferIds;
 
+/* ResourceOwner callbacks to hold buffer pins */
+extern ResourceOwnerFuncs buffer_resowner_funcs;
+
+/* Convenience wrappers over ResourceOwnerRemember/Forget */
+#define ResourceOwnerRememberBuffer(owner, buffer) \
+	ResourceOwnerRemember(owner, Int32GetDatum(buffer), &buffer_resowner_funcs)
+#define ResourceOwnerForgetBuffer(owner, buffer) \
+	ResourceOwnerForget(owner, Int32GetDatum(buffer), &buffer_resowner_funcs)
+
 /*
  * Internal buffer management routines
  */
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index ddc2762eb3f..2bf95c22e84 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -225,7 +225,4 @@ extern void PrepareToInvalidateCacheTuple(Relation relation,
 										  HeapTuple newtuple,
 										  void (*function) (int, uint32, Oid));
 
-extern void PrintCatCacheLeakWarning(HeapTuple tuple);
-extern void PrintCatCacheListLeakWarning(CatCList *list);
-
 #endif							/* CATCACHE_H */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 79d96e5ed03..964af79b30d 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -233,4 +233,6 @@ extern bool CachedPlanIsSimplyValid(CachedPlanSource *plansource,
 extern CachedExpression *GetCachedExpression(Node *expr);
 extern void FreeCachedExpression(CachedExpression *cexpr);
 
+extern ResourceOwnerFuncs planref_resowner_funcs;
+
 #endif							/* PLANCACHE_H */
diff --git a/src/include/utils/resowner.h b/src/include/utils/resowner.h
index 109ac31b248..c3f1f060fa9 100644
--- a/src/include/utils/resowner.h
+++ b/src/include/utils/resowner.h
@@ -50,6 +50,32 @@ typedef enum
 	RESOURCE_RELEASE_AFTER_LOCKS
 } ResourceReleasePhase;
 
+/*
+ * In order to track an object, resowner.c needs a few callbacks for it.
+ * The callbacks for an object of a specific kind are encapsulated in
+ * ResourceOwnerFuncs.
+ */
+typedef struct ResourceOwnerFuncs
+{
+	const char *name;		/* name for the object kind, for debugging */
+	ResourceReleasePhase phase; /* when are these objects released? */
+
+	/*
+	 * Release resource.
+	 *
+	 * NOTE: this must call ResourceOwnerForget to disassociate it with the
+	 * resource owner.
+	 */
+	void (*ReleaseResource)(Datum res);
+
+	/*
+	 * Print a warning, when a resource has not been properly released before
+	 * commit.
+	 */
+	void (*PrintLeakWarning)(Datum res);
+
+} ResourceOwnerFuncs;
+
 /*
  *	Dynamically loaded modules can get control during ResourceOwnerRelease
  *	by providing a callback of this form.
@@ -71,16 +97,29 @@ extern void ResourceOwnerRelease(ResourceOwner owner,
 								 ResourceReleasePhase phase,
 								 bool isCommit,
 								 bool isTopLevel);
-extern void ResourceOwnerReleaseAllPlanCacheRefs(ResourceOwner owner);
 extern void ResourceOwnerDelete(ResourceOwner owner);
 extern ResourceOwner ResourceOwnerGetParent(ResourceOwner owner);
 extern void ResourceOwnerNewParent(ResourceOwner owner,
 								   ResourceOwner newparent);
+
+extern void ResourceOwnerEnlarge(ResourceOwner owner);
+extern void ResourceOwnerRemember(ResourceOwner owner, Datum res, ResourceOwnerFuncs *kind);
+extern void ResourceOwnerForget(ResourceOwner owner, Datum res, ResourceOwnerFuncs *kind);
+
 extern void RegisterResourceReleaseCallback(ResourceReleaseCallback callback,
 											void *arg);
 extern void UnregisterResourceReleaseCallback(ResourceReleaseCallback callback,
 											  void *arg);
+
 extern void CreateAuxProcessResourceOwner(void);
 extern void ReleaseAuxProcessResources(bool isCommit);
 
+/* special support for local lock management */
+struct LOCALLOCK;
+extern void ResourceOwnerRememberLock(ResourceOwner owner, struct LOCALLOCK *locallock);
+extern void ResourceOwnerForgetLock(ResourceOwner owner, struct LOCALLOCK *locallock);
+
+/* special function to relase all plancache references */
+extern void ResourceOwnerReleaseAllPlanCacheRefs(ResourceOwner owner);
+
 #endif							/* RESOWNER_H */
diff --git a/src/include/utils/resowner_private.h b/src/include/utils/resowner_private.h
deleted file mode 100644
index c480a1a24be..00000000000
--- a/src/include/utils/resowner_private.h
+++ /dev/null
@@ -1,105 +0,0 @@
-/*-------------------------------------------------------------------------
- *
- * resowner_private.h
- *	  POSTGRES resource owner private definitions.
- *
- * See utils/resowner/README for more info.
- *
- *
- * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
- *
- * src/include/utils/resowner_private.h
- *
- *-------------------------------------------------------------------------
- */
-#ifndef RESOWNER_PRIVATE_H
-#define RESOWNER_PRIVATE_H
-
-#include "storage/dsm.h"
-#include "storage/fd.h"
-#include "storage/lock.h"
-#include "utils/catcache.h"
-#include "utils/plancache.h"
-#include "utils/resowner.h"
-#include "utils/snapshot.h"
-
-
-/* support for buffer refcount management */
-extern void ResourceOwnerEnlargeBuffers(ResourceOwner owner);
-extern void ResourceOwnerRememberBuffer(ResourceOwner owner, Buffer buffer);
-extern void ResourceOwnerForgetBuffer(ResourceOwner owner, Buffer buffer);
-
-/* support for local lock management */
-extern void ResourceOwnerRememberLock(ResourceOwner owner, LOCALLOCK *locallock);
-extern void ResourceOwnerForgetLock(ResourceOwner owner, LOCALLOCK *locallock);
-
-/* support for catcache refcount management */
-extern void ResourceOwnerEnlargeCatCacheRefs(ResourceOwner owner);
-extern void ResourceOwnerRememberCatCacheRef(ResourceOwner owner,
-											 HeapTuple tuple);
-extern void ResourceOwnerForgetCatCacheRef(ResourceOwner owner,
-										   HeapTuple tuple);
-extern void ResourceOwnerEnlargeCatCacheListRefs(ResourceOwner owner);
-extern void ResourceOwnerRememberCatCacheListRef(ResourceOwner owner,
-												 CatCList *list);
-extern void ResourceOwnerForgetCatCacheListRef(ResourceOwner owner,
-											   CatCList *list);
-
-/* support for relcache refcount management */
-extern void ResourceOwnerEnlargeRelationRefs(ResourceOwner owner);
-extern void ResourceOwnerRememberRelationRef(ResourceOwner owner,
-											 Relation rel);
-extern void ResourceOwnerForgetRelationRef(ResourceOwner owner,
-										   Relation rel);
-
-/* support for plancache refcount management */
-extern void ResourceOwnerEnlargePlanCacheRefs(ResourceOwner owner);
-extern void ResourceOwnerRememberPlanCacheRef(ResourceOwner owner,
-											  CachedPlan *plan);
-extern void ResourceOwnerForgetPlanCacheRef(ResourceOwner owner,
-											CachedPlan *plan);
-
-/* support for tupledesc refcount management */
-extern void ResourceOwnerEnlargeTupleDescs(ResourceOwner owner);
-extern void ResourceOwnerRememberTupleDesc(ResourceOwner owner,
-										   TupleDesc tupdesc);
-extern void ResourceOwnerForgetTupleDesc(ResourceOwner owner,
-										 TupleDesc tupdesc);
-
-/* support for snapshot refcount management */
-extern void ResourceOwnerEnlargeSnapshots(ResourceOwner owner);
-extern void ResourceOwnerRememberSnapshot(ResourceOwner owner,
-										  Snapshot snapshot);
-extern void ResourceOwnerForgetSnapshot(ResourceOwner owner,
-										Snapshot snapshot);
-
-/* support for temporary file management */
-extern void ResourceOwnerEnlargeFiles(ResourceOwner owner);
-extern void ResourceOwnerRememberFile(ResourceOwner owner,
-									  File file);
-extern void ResourceOwnerForgetFile(ResourceOwner owner,
-									File file);
-
-/* support for dynamic shared memory management */
-extern void ResourceOwnerEnlargeDSMs(ResourceOwner owner);
-extern void ResourceOwnerRememberDSM(ResourceOwner owner,
-									 dsm_segment *);
-extern void ResourceOwnerForgetDSM(ResourceOwner owner,
-								   dsm_segment *);
-
-/* support for JITContext management */
-extern void ResourceOwnerEnlargeJIT(ResourceOwner owner);
-extern void ResourceOwnerRememberJIT(ResourceOwner owner,
-									 Datum handle);
-extern void ResourceOwnerForgetJIT(ResourceOwner owner,
-								   Datum handle);
-
-/* support for cryptohash context management */
-extern void ResourceOwnerEnlargeCryptoHash(ResourceOwner owner);
-extern void ResourceOwnerRememberCryptoHash(ResourceOwner owner,
-											Datum handle);
-extern void ResourceOwnerForgetCryptoHash(ResourceOwner owner,
-										  Datum handle);
-
-#endif							/* RESOWNER_PRIVATE_H */
-- 
2.29.2


--------------7349B9D66ED595EF3B1030B4--





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

* [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-02-26 08:17  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-02-26 08:17 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 src/backend/access/transam/parallel.c         |   8 ++
 src/backend/access/transam/xact.c             | 106 ++++++++++++++---
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 107 ++++++++++++++----
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/standby.c             |   4 +-
 src/include/access/xlog.h                     |  15 ++-
 src/include/commands/cluster.h                |   1 +
 src/include/utils/rel.h                       |   6 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 12 files changed, 216 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 4ab5df9213..4e6e92c4db 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -97,6 +97,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -351,6 +352,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1546,6 +1548,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 0bfd329847..0e4f234440 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -137,6 +138,12 @@ static TransactionId *ParallelCurrentXids;
 static int				 nRepackCurrentXids = 0;
 static TransactionId	*RepackCurrentXids = NULL;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -648,6 +655,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -662,6 +670,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -691,20 +725,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -729,6 +749,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2243,6 +2311,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 799fc739e1..4a8a43a6da 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index a5790d77b5..910ff9fa91 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1298,7 +1298,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 	 *
 	 * In the REPACK CONCURRENTLY case, the lock does not help because we need
 	 * to release it temporarily at some point. Instead, we expect VACUUM /
-	 * CLUSTER to skip tables which are present in RepackedRelsHash.
+	 * CLUSTER to skip tables which are present in repackedRels->hashtable.
 	 */
 	if (OldHeap->rd_rel->reltoastrelid && !concurrent)
 		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
@@ -2312,7 +2312,16 @@ typedef struct RepackedRel
 	Oid		dbid;
 } RepackedRel;
 
-static HTAB *RepackedRelsHash = NULL;
+typedef struct RepackedRels
+{
+	/* Hashtable of RepackedRel elements. */
+	HTAB	*hashtable;
+
+	/* The number of elements in the hashtable.. */
+	pg_atomic_uint32	nrels;
+} RepackedRels;
+
+static RepackedRels *repackedRels = NULL;
 
 /* Maximum number of entries in the hashtable. */
 static int maxRepackedRels = 0;
@@ -2320,28 +2329,44 @@ static int maxRepackedRels = 0;
 Size
 RepackShmemSize(void)
 {
+	Size	result;
+
+	result = sizeof(RepackedRels);
+
 	/*
 	 * A replication slot is needed for the processing, so use this GUC to
 	 * allocate memory for the hashtable.
 	 */
 	maxRepackedRels = max_replication_slots;
 
-	return hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	result += hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	return result;
 }
 
 void
 RepackShmemInit(void)
 {
+	bool		found;
 	HASHCTL		info;
 
+	repackedRels = ShmemInitStruct("Repacked Relations",
+								   sizeof(RepackedRels),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		pg_atomic_init_u32(&repackedRels->nrels, 0);
+	}
+	else
+		Assert(found);
+
 	info.keysize = sizeof(RepackedRel);
 	info.entrysize = info.keysize;
-
-	RepackedRelsHash = ShmemInitHash("Repacked Relations",
-									 maxRepackedRels,
-									 maxRepackedRels,
-									 &info,
-									 HASH_ELEM | HASH_BLOBS);
+	repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash",
+											maxRepackedRels,
+											maxRepackedRels,
+											&info,
+											HASH_ELEM | HASH_BLOBS);
 }
 
 /*
@@ -2373,12 +2398,13 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	RelReopenInfo	rri[2];
 	int		nrel;
 	static bool before_shmem_exit_callback_setup = false;
+	uint32	nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	relid = RelationGetRelid(rel);
 
 	/*
-	 * Make sure that we do not leave an entry in RepackedRelsHash if exiting
-	 * due to FATAL.
+	 * Make sure that we do not leave an entry in repackedRels->Hashtable if
+	 * exiting due to FATAL.
 	 */
 	if (!before_shmem_exit_callback_setup)
 	{
@@ -2393,7 +2419,7 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	*entered_p = false;
 	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found);
 	if (found)
 	{
 		/*
@@ -2411,6 +2437,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+	/* Increment the number of relations. */
+	nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+	Assert(nrels < maxRepackedRels);
+
 	/*
 	 * Even if the insertion of TOAST relid should fail below, the caller has
 	 * to do cleanup.
@@ -2438,7 +2468,8 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	{
 		key.relid = toastrelid;
 		entry = (RepackedRel *)
-			hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+			hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL,
+						&found);
 		if (found)
 			/*
 			 * If we could enter the main fork the TOAST should succeed
@@ -2452,6 +2483,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+		/* Increment the number of relations. */
+		nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+		Assert(nrels < maxRepackedRels);
+
 		Assert(!OidIsValid(repacked_rel_toast));
 		repacked_rel_toast = toastrelid;
 	}
@@ -2531,6 +2566,7 @@ end_concurrent_repack(bool error)
 	RepackedRel	*entry = NULL, *entry_toast = NULL;
 	Oid		relid = repacked_rel;
 	Oid		toastrelid = repacked_rel_toast;
+	uint32	nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	/* Remove the relation from the hash if we managed to insert one. */
 	if (OidIsValid(repacked_rel))
@@ -2539,23 +2575,32 @@ end_concurrent_repack(bool error)
 		key.relid = repacked_rel;
 		key.dbid = MyDatabaseId;
 		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
-		entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL);
+		entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
+							NULL);
 
 		/*
 		 * By clearing this variable we also disable
 		 * cluster_before_shmem_exit_callback().
 		 */
 		repacked_rel = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 
 	/* Remove the TOAST relation if there is one. */
 	if (OidIsValid(repacked_rel_toast))
 	{
 		key.relid = repacked_rel_toast;
-		entry_toast = hash_search(RepackedRelsHash, &key, HASH_REMOVE,
+		entry_toast = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
 								  NULL);
 
 		repacked_rel_toast = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 	LWLockRelease(RepackedRelsLock);
 
@@ -2621,7 +2666,7 @@ end_concurrent_repack(bool error)
 }
 
 /*
- * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ * A wrapper to call end_concurrent_cluster() as a before_shmem_exit callback.
  */
 static void
 cluster_before_shmem_exit_callback(int code, Datum arg)
@@ -2632,24 +2677,48 @@ cluster_before_shmem_exit_callback(int code, Datum arg)
 
 /*
  * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
  */
 bool
 is_concurrent_repack_in_progress(Oid relid)
 {
 	RepackedRel	key, *entry;
 
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just use the counter.
+	 */
+	if (!OidIsValid(relid))
+	{
+		if (pg_atomic_read_u32(&repackedRels->nrels) > 0)
+			return true;
+		else
+			return false;
+	}
+
+	/* For particular relation we need to search in the hashtable. */
 	memset(&key, 0, sizeof(key));
 	key.relid = relid;
 	key.dbid = MyDatabaseId;
 
 	LWLockAcquire(RepackedRelsLock, LW_SHARED);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_FIND, NULL);
+		hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL);
 	LWLockRelease(RepackedRelsLock);
 
 	return entry != NULL;
 }
 
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
+}
+
 /*
  * Check if REPACK CONCURRENTLY is already running for given relation, and if
  * so, raise ERROR. The problem is that cluster_rel() needs to release its
@@ -2944,8 +3013,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
-	 * regarding logical decoding, treated like a catalog.
+	 * table we are processing is present in repackedRels->hashtable and
+	 * therefore, regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
 									NIL,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 8ea846bfc3..e5790d3fe8 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 5acb4508f8..413bcc1add 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1313,13 +1313,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c02..a325bb1d16 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index f5600bf4f6..3ed3066b36 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -173,6 +173,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 extern Size RepackShmemSize(void);
 extern void RepackShmemInit(void);
 extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
 extern void check_for_concurrent_repack(Oid relid, LOCKMODE lockmode);
 
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 741b29226d..81f5348a4f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -709,12 +709,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4..4f6c0ca3a8 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6c..0000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba99..716e5619aa 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
-- 
2.43.5


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v08-0009-Call-logical_rewrite_heap_tuple-when-applying-concur.patch



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

* [PATCH v09 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-02-26 08:17  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-02-26 08:17 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 src/backend/access/transam/parallel.c         |   8 ++
 src/backend/access/transam/xact.c             | 106 ++++++++++++++---
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 107 ++++++++++++++----
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/standby.c             |   4 +-
 src/include/access/xlog.h                     |  15 ++-
 src/include/commands/cluster.h                |   1 +
 src/include/utils/rel.h                       |   6 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 12 files changed, 216 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e766be7b81d..479fe62b1c7 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -138,6 +139,12 @@ static TransactionId *ParallelCurrentXids;
 static int				 nRepackCurrentXids = 0;
 static TransactionId	*RepackCurrentXids = NULL;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -649,6 +656,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -663,6 +671,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -692,20 +726,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -730,6 +750,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2244,6 +2312,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 4b6c694a3f7..1b131e1436f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 0a2bacbb1df..fa6db0932c0 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1298,7 +1298,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 	 *
 	 * In the REPACK CONCURRENTLY case, the lock does not help because we need
 	 * to release it temporarily at some point. Instead, we expect VACUUM /
-	 * CLUSTER to skip tables which are present in RepackedRelsHash.
+	 * CLUSTER to skip tables which are present in repackedRels->hashtable.
 	 */
 	if (OldHeap->rd_rel->reltoastrelid && !concurrent)
 		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
@@ -2317,7 +2317,16 @@ typedef struct RepackedRel
 	Oid		dbid;
 } RepackedRel;
 
-static HTAB *RepackedRelsHash = NULL;
+typedef struct RepackedRels
+{
+	/* Hashtable of RepackedRel elements. */
+	HTAB	*hashtable;
+
+	/* The number of elements in the hashtable.. */
+	pg_atomic_uint32	nrels;
+} RepackedRels;
+
+static RepackedRels *repackedRels = NULL;
 
 /* Maximum number of entries in the hashtable. */
 static int maxRepackedRels = 0;
@@ -2325,28 +2334,44 @@ static int maxRepackedRels = 0;
 Size
 RepackShmemSize(void)
 {
+	Size	result;
+
+	result = sizeof(RepackedRels);
+
 	/*
 	 * A replication slot is needed for the processing, so use this GUC to
 	 * allocate memory for the hashtable.
 	 */
 	maxRepackedRels = max_replication_slots;
 
-	return hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	result += hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	return result;
 }
 
 void
 RepackShmemInit(void)
 {
+	bool		found;
 	HASHCTL		info;
 
+	repackedRels = ShmemInitStruct("Repacked Relations",
+								   sizeof(RepackedRels),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		pg_atomic_init_u32(&repackedRels->nrels, 0);
+	}
+	else
+		Assert(found);
+
 	info.keysize = sizeof(RepackedRel);
 	info.entrysize = info.keysize;
-
-	RepackedRelsHash = ShmemInitHash("Repacked Relations",
-									 maxRepackedRels,
-									 maxRepackedRels,
-									 &info,
-									 HASH_ELEM | HASH_BLOBS);
+	repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash",
+											maxRepackedRels,
+											maxRepackedRels,
+											&info,
+											HASH_ELEM | HASH_BLOBS);
 }
 
 /*
@@ -2378,12 +2403,13 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	RelReopenInfo	rri[2];
 	int		nrel;
 	static bool before_shmem_exit_callback_setup = false;
+	uint32	nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	relid = RelationGetRelid(rel);
 
 	/*
-	 * Make sure that we do not leave an entry in RepackedRelsHash if exiting
-	 * due to FATAL.
+	 * Make sure that we do not leave an entry in repackedRels->Hashtable if
+	 * exiting due to FATAL.
 	 */
 	if (!before_shmem_exit_callback_setup)
 	{
@@ -2398,7 +2424,7 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	*entered_p = false;
 	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found);
 	if (found)
 	{
 		/*
@@ -2416,6 +2442,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+	/* Increment the number of relations. */
+	nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+	Assert(nrels < maxRepackedRels);
+
 	/*
 	 * Even if the insertion of TOAST relid should fail below, the caller has
 	 * to do cleanup.
@@ -2443,7 +2473,8 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	{
 		key.relid = toastrelid;
 		entry = (RepackedRel *)
-			hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+			hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL,
+						&found);
 		if (found)
 			/*
 			 * If we could enter the main fork the TOAST should succeed
@@ -2457,6 +2488,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+		/* Increment the number of relations. */
+		nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+		Assert(nrels < maxRepackedRels);
+
 		Assert(!OidIsValid(repacked_rel_toast));
 		repacked_rel_toast = toastrelid;
 	}
@@ -2536,6 +2571,7 @@ end_concurrent_repack(bool error)
 	RepackedRel	*entry = NULL, *entry_toast = NULL;
 	Oid		relid = repacked_rel;
 	Oid		toastrelid = repacked_rel_toast;
+	uint32	nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	/* Remove the relation from the hash if we managed to insert one. */
 	if (OidIsValid(repacked_rel))
@@ -2544,23 +2580,32 @@ end_concurrent_repack(bool error)
 		key.relid = repacked_rel;
 		key.dbid = MyDatabaseId;
 		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
-		entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL);
+		entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
+							NULL);
 
 		/*
 		 * By clearing this variable we also disable
 		 * cluster_before_shmem_exit_callback().
 		 */
 		repacked_rel = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 
 	/* Remove the TOAST relation if there is one. */
 	if (OidIsValid(repacked_rel_toast))
 	{
 		key.relid = repacked_rel_toast;
-		entry_toast = hash_search(RepackedRelsHash, &key, HASH_REMOVE,
+		entry_toast = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
 								  NULL);
 
 		repacked_rel_toast = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 	LWLockRelease(RepackedRelsLock);
 
@@ -2626,7 +2671,7 @@ end_concurrent_repack(bool error)
 }
 
 /*
- * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ * A wrapper to call end_concurrent_cluster() as a before_shmem_exit callback.
  */
 static void
 cluster_before_shmem_exit_callback(int code, Datum arg)
@@ -2637,24 +2682,48 @@ cluster_before_shmem_exit_callback(int code, Datum arg)
 
 /*
  * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
  */
 bool
 is_concurrent_repack_in_progress(Oid relid)
 {
 	RepackedRel	key, *entry;
 
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just use the counter.
+	 */
+	if (!OidIsValid(relid))
+	{
+		if (pg_atomic_read_u32(&repackedRels->nrels) > 0)
+			return true;
+		else
+			return false;
+	}
+
+	/* For particular relation we need to search in the hashtable. */
 	memset(&key, 0, sizeof(key));
 	key.relid = relid;
 	key.dbid = MyDatabaseId;
 
 	LWLockAcquire(RepackedRelsLock, LW_SHARED);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_FIND, NULL);
+		hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL);
 	LWLockRelease(RepackedRelsLock);
 
 	return entry != NULL;
 }
 
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
+}
+
 /*
  * Check if REPACK CONCURRENTLY is already running for given relation, and if
  * so, raise ERROR. The problem is that cluster_rel() needs to release its
@@ -2949,8 +3018,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
-	 * regarding logical decoding, treated like a catalog.
+	 * table we are processing is present in repackedRels->hashtable and
+	 * therefore, regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
 									NIL,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 8ea846bfc3b..e5790d3fe84 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 5acb4508f85..413bcc1addb 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1313,13 +1313,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index f5600bf4f62..3ed3066b364 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -173,6 +173,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 extern Size RepackShmemSize(void);
 extern void RepackShmemInit(void);
 extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
 extern void check_for_concurrent_repack(Oid relid, LOCKMODE lockmode);
 
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index cb485d26f44..88bacc109ff 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -710,12 +710,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
-- 
2.39.5


--v2l46qwn2nggemiy
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
	filename="v09-0009-Call-logical_rewrite_heap_tuple-when-applying-co.patch"



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

* [PATCH v10 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-03-24 19:17  Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Álvaro Herrera @ 2025-03-24 19:17 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 src/backend/access/transam/parallel.c         |   8 ++
 src/backend/access/transam/xact.c             | 106 ++++++++++++++---
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 107 ++++++++++++++----
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/standby.c             |   4 +-
 src/include/access/xlog.h                     |  15 ++-
 src/include/commands/cluster.h                |   1 +
 src/include/utils/rel.h                       |   6 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 13 files changed, 217 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index aebad612ce8..1b02ef0bacb 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -138,6 +139,12 @@ static TransactionId *ParallelCurrentXids;
 static int	nRepackCurrentXids = 0;
 static TransactionId *RepackCurrentXids = NULL;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -649,6 +656,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -663,6 +671,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -692,20 +726,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -730,6 +750,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2244,6 +2312,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 4b6c694a3f7..1b131e1436f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index c272ed03cb9..2ab1756fa19 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1304,7 +1304,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 	 *
 	 * In the REPACK CONCURRENTLY case, the lock does not help because we need
 	 * to release it temporarily at some point. Instead, we expect VACUUM /
-	 * CLUSTER to skip tables which are present in RepackedRelsHash.
+	 * CLUSTER to skip tables which are present in repackedRels->hashtable.
 	 */
 	if (OldHeap->rd_rel->reltoastrelid && !concurrent)
 		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
@@ -2324,7 +2324,16 @@ typedef struct RepackedRel
 	Oid			dbid;
 } RepackedRel;
 
-static HTAB *RepackedRelsHash = NULL;
+typedef struct RepackedRels
+{
+	/* Hashtable of RepackedRel elements. */
+	HTAB	   *hashtable;
+
+	/* The number of elements in the hashtable.. */
+	pg_atomic_uint32 nrels;
+} RepackedRels;
+
+static RepackedRels *repackedRels = NULL;
 
 /* Maximum number of entries in the hashtable. */
 static int	maxRepackedRels = 0;
@@ -2332,28 +2341,44 @@ static int	maxRepackedRels = 0;
 Size
 RepackShmemSize(void)
 {
+	Size		result;
+
+	result = sizeof(RepackedRels);
+
 	/*
 	 * A replication slot is needed for the processing, so use this GUC to
 	 * allocate memory for the hashtable.
 	 */
 	maxRepackedRels = max_replication_slots;
 
-	return hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	result += hash_estimate_size(maxRepackedRels, sizeof(RepackedRel));
+	return result;
 }
 
 void
 RepackShmemInit(void)
 {
+	bool		found;
 	HASHCTL		info;
 
+	repackedRels = ShmemInitStruct("Repacked Relations",
+								   sizeof(RepackedRels),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		pg_atomic_init_u32(&repackedRels->nrels, 0);
+	}
+	else
+		Assert(found);
+
 	info.keysize = sizeof(RepackedRel);
 	info.entrysize = info.keysize;
-
-	RepackedRelsHash = ShmemInitHash("Repacked Relations",
-									 maxRepackedRels,
-									 maxRepackedRels,
-									 &info,
-									 HASH_ELEM | HASH_BLOBS);
+	repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash",
+											maxRepackedRels,
+											maxRepackedRels,
+											&info,
+											HASH_ELEM | HASH_BLOBS);
 }
 
 /*
@@ -2387,12 +2412,13 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	RelReopenInfo rri[2];
 	int			nrel;
 	static bool before_shmem_exit_callback_setup = false;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	relid = RelationGetRelid(rel);
 
 	/*
-	 * Make sure that we do not leave an entry in RepackedRelsHash if exiting
-	 * due to FATAL.
+	 * Make sure that we do not leave an entry in repackedRels->Hashtable if
+	 * exiting due to FATAL.
 	 */
 	if (!before_shmem_exit_callback_setup)
 	{
@@ -2407,7 +2433,7 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	*entered_p = false;
 	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found);
 	if (found)
 	{
 		/*
@@ -2425,6 +2451,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+	/* Increment the number of relations. */
+	nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+	Assert(nrels < maxRepackedRels);
+
 	/*
 	 * Even if the insertion of TOAST relid should fail below, the caller has
 	 * to do cleanup.
@@ -2452,7 +2482,8 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 	{
 		key.relid = toastrelid;
 		entry = (RepackedRel *)
-			hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+			hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL,
+						&found);
 		if (found)
 
 			/*
@@ -2467,6 +2498,10 @@ begin_concurrent_repack(Relation *rel_p, Relation *index_p,
 					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+		/* Increment the number of relations. */
+		nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+		Assert(nrels < maxRepackedRels);
+
 		Assert(!OidIsValid(repacked_rel_toast));
 		repacked_rel_toast = toastrelid;
 	}
@@ -2549,6 +2584,7 @@ end_concurrent_repack(bool error)
 			   *entry_toast = NULL;
 	Oid			relid = repacked_rel;
 	Oid			toastrelid = repacked_rel_toast;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	/* Remove the relation from the hash if we managed to insert one. */
 	if (OidIsValid(repacked_rel))
@@ -2557,23 +2593,32 @@ end_concurrent_repack(bool error)
 		key.relid = repacked_rel;
 		key.dbid = MyDatabaseId;
 		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
-		entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL);
+		entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
+							NULL);
 
 		/*
 		 * By clearing this variable we also disable
 		 * cluster_before_shmem_exit_callback().
 		 */
 		repacked_rel = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 
 	/* Remove the TOAST relation if there is one. */
 	if (OidIsValid(repacked_rel_toast))
 	{
 		key.relid = repacked_rel_toast;
-		entry_toast = hash_search(RepackedRelsHash, &key, HASH_REMOVE,
+		entry_toast = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
 								  NULL);
 
 		repacked_rel_toast = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 	LWLockRelease(RepackedRelsLock);
 
@@ -2639,7 +2684,7 @@ end_concurrent_repack(bool error)
 }
 
 /*
- * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ * A wrapper to call end_concurrent_cluster() as a before_shmem_exit callback.
  */
 static void
 cluster_before_shmem_exit_callback(int code, Datum arg)
@@ -2650,6 +2695,8 @@ cluster_before_shmem_exit_callback(int code, Datum arg)
 
 /*
  * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
  */
 bool
 is_concurrent_repack_in_progress(Oid relid)
@@ -2657,18 +2704,40 @@ is_concurrent_repack_in_progress(Oid relid)
 	RepackedRel key,
 			   *entry;
 
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just use the counter.
+	 */
+	if (!OidIsValid(relid))
+	{
+		if (pg_atomic_read_u32(&repackedRels->nrels) > 0)
+			return true;
+		else
+			return false;
+	}
+
+	/* For particular relation we need to search in the hashtable. */
 	memset(&key, 0, sizeof(key));
 	key.relid = relid;
 	key.dbid = MyDatabaseId;
 
 	LWLockAcquire(RepackedRelsLock, LW_SHARED);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_FIND, NULL);
+		hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL);
 	LWLockRelease(RepackedRelsLock);
 
 	return entry != NULL;
 }
 
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
+}
+
 /*
  * Check if REPACK CONCURRENTLY is already running for given relation, and if
  * so, raise ERROR. The problem is that cluster_rel() needs to release its
@@ -2967,8 +3036,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
-	 * regarding logical decoding, treated like a catalog.
+	 * table we are processing is present in repackedRels->hashtable and
+	 * therefore, regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
 									NIL,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 8ea846bfc3b..e5790d3fe84 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 5acb4508f85..413bcc1addb 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1313,13 +1313,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 268c3098512..6f5566210a8 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -173,6 +173,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 extern Size RepackShmemSize(void);
 extern void RepackShmemInit(void);
 extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
 extern void check_for_concurrent_repack(Oid relid, LOCKMODE lockmode);
 
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 372065fc570..fcbad5c1720 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -710,12 +710,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ac52b8b0336..de1a178a4b9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2508,6 +2508,7 @@ ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
 RepackedRel
+RepackedRels
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.39.5


--w2lc3gf2kw4enm6y
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
	filename="v10-0009-Call-logical_rewrite_heap_tuple-when-applying-co.patch"



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

* [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-03-31 13:47  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-03-31 13:47 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 src/backend/access/transam/parallel.c         |   8 ++
 src/backend/access/transam/xact.c             | 106 +++++++++++++++---
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                |  94 +++++++++++++---
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/standby.c             |   4 +-
 src/include/access/xlog.h                     |  15 ++-
 src/include/commands/cluster.h                |   1 +
 src/include/utils/rel.h                       |   6 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 13 files changed, 206 insertions(+), 44 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 3db4cac030e..608dc5c79bb 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -138,6 +139,12 @@ static TransactionId *ParallelCurrentXids;
 static int	nRepackCurrentXids = 0;
 static TransactionId *RepackCurrentXids = NULL;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -650,6 +657,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -664,6 +672,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -693,20 +727,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -731,6 +751,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2245,6 +2313,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fc30a52d496..ba758deefb4 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 3f07b42d615..734e47eaba3 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -2203,7 +2203,16 @@ typedef struct RepackedRel
 	Oid			dbid;
 } RepackedRel;
 
-static HTAB *RepackedRelsHash = NULL;
+typedef struct RepackedRels
+{
+	/* Hashtable of RepackedRel elements. */
+	HTAB	   *hashtable;
+
+	/* The number of elements in the hashtable.. */
+	pg_atomic_uint32 nrels;
+} RepackedRels;
+
+static RepackedRels *repackedRels = NULL;
 
 /*
  * Maximum number of entries in the hashtable.
@@ -2216,22 +2225,38 @@ static HTAB *RepackedRelsHash = NULL;
 Size
 RepackShmemSize(void)
 {
-	return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+	Size		result;
+
+	result = sizeof(RepackedRels);
+
+	result += hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+	return result;
 }
 
 void
 RepackShmemInit(void)
 {
+	bool		found;
 	HASHCTL		info;
 
+	repackedRels = ShmemInitStruct("Repacked Relations",
+								   sizeof(RepackedRels),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		pg_atomic_init_u32(&repackedRels->nrels, 0);
+	}
+	else
+		Assert(found);
+
 	info.keysize = sizeof(RepackedRel);
 	info.entrysize = info.keysize;
-
-	RepackedRelsHash = ShmemInitHash("Repacked Relations",
-									 MAX_REPACKED_RELS,
-									 MAX_REPACKED_RELS,
-									 &info,
-									 HASH_ELEM | HASH_BLOBS);
+	repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash",
+											MAX_REPACKED_RELS,
+											MAX_REPACKED_RELS,
+											&info,
+											HASH_ELEM | HASH_BLOBS);
 }
 
 /*
@@ -2266,13 +2291,14 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 			   *entry;
 	bool		found;
 	static bool before_shmem_exit_callback_setup = false;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	relid = RelationGetRelid(rel);
 	index = index_p ? *index_p : NULL;
 
 	/*
-	 * Make sure that we do not leave an entry in RepackedRelsHash if exiting
-	 * due to FATAL.
+	 * Make sure that we do not leave an entry in repackedRels->hashtable if
+	 * exiting due to FATAL.
 	 */
 	if (!before_shmem_exit_callback_setup)
 	{
@@ -2287,7 +2313,7 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 	*entered_p = false;
 	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found);
 	if (found)
 	{
 		/*
@@ -2305,9 +2331,13 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+	/* Increment the number of relations. */
+	nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+	Assert(nrels < MAX_REPACKED_RELS);
+
 	/*
-	 * Even if anything fails below, the caller has to do cleanup in the
-	 * shared memory.
+	 * Even if the insertion of TOAST relid should fail below, the caller has
+	 * to do cleanup.
 	 */
 	*entered_p = true;
 
@@ -2389,6 +2419,7 @@ end_concurrent_repack(bool error)
 	RepackedRel key;
 	RepackedRel *entry = NULL;
 	Oid			relid = repacked_rel;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	/* Remove the relation from the hash if we managed to insert one. */
 	if (OidIsValid(repacked_rel))
@@ -2397,7 +2428,8 @@ end_concurrent_repack(bool error)
 		key.relid = repacked_rel;
 		key.dbid = MyDatabaseId;
 		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
-		entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL);
+		entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
+							NULL);
 		LWLockRelease(RepackedRelsLock);
 
 		/*
@@ -2426,6 +2458,10 @@ end_concurrent_repack(bool error)
 		 * cluster_before_shmem_exit_callback().
 		 */
 		repacked_rel = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 
 	/*
@@ -2478,6 +2514,8 @@ cluster_before_shmem_exit_callback(int code, Datum arg)
 
 /*
  * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
  */
 bool
 is_concurrent_repack_in_progress(Oid relid)
@@ -2485,18 +2523,40 @@ is_concurrent_repack_in_progress(Oid relid)
 	RepackedRel key,
 			   *entry;
 
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just use the counter.
+	 */
+	if (!OidIsValid(relid))
+	{
+		if (pg_atomic_read_u32(&repackedRels->nrels) > 0)
+			return true;
+		else
+			return false;
+	}
+
+	/* For particular relation we need to search in the hashtable. */
 	memset(&key, 0, sizeof(key));
 	key.relid = relid;
 	key.dbid = MyDatabaseId;
 
 	LWLockAcquire(RepackedRelsLock, LW_SHARED);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_FIND, NULL);
+		hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL);
 	LWLockRelease(RepackedRelsLock);
 
 	return entry != NULL;
 }
 
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
+}
+
 /*
  * This function is much like pg_create_logical_replication_slot() except that
  * the new slot is neither released (if anyone else could read changes from
@@ -2524,8 +2584,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
-	 * regarding logical decoding, treated like a catalog.
+	 * table we are processing is present in repackedRels->hashtable and
+	 * therefore, regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
 									NIL,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index a8d2e024d34..4909432d585 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 5acb4508f85..413bcc1addb 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1313,13 +1313,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index be283c70fce..0267357a261 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -172,6 +172,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 extern Size RepackShmemSize(void);
 extern void RepackShmemInit(void);
 extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
 
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 #endif							/* CLUSTER_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 372065fc570..fcbad5c1720 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -710,12 +710,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1e3e619c4b..b3be8572132 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2511,6 +2511,7 @@ ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
 RepackedRel
+RepackedRels
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.43.5


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v11-0009-Call-logical_rewrite_heap_tuple-when-applying-concur.patch



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

* [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-04-01 11:48  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-04-01 11:48 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 src/backend/access/transam/parallel.c         |   8 ++
 src/backend/access/transam/xact.c             | 106 +++++++++++++++---
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                |  94 +++++++++++++---
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/standby.c             |   4 +-
 src/include/access/xlog.h                     |  15 ++-
 src/include/commands/cluster.h                |   1 +
 src/include/utils/rel.h                       |   6 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 13 files changed, 206 insertions(+), 44 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 3db4cac030e..608dc5c79bb 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -138,6 +139,12 @@ static TransactionId *ParallelCurrentXids;
 static int	nRepackCurrentXids = 0;
 static TransactionId *RepackCurrentXids = NULL;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -650,6 +657,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -664,6 +672,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -693,20 +727,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -731,6 +751,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2245,6 +2313,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fc30a52d496..ba758deefb4 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 085649716ac..c2201b046bc 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -2204,7 +2204,16 @@ typedef struct RepackedRel
 	Oid			dbid;
 } RepackedRel;
 
-static HTAB *RepackedRelsHash = NULL;
+typedef struct RepackedRels
+{
+	/* Hashtable of RepackedRel elements. */
+	HTAB	   *hashtable;
+
+	/* The number of elements in the hashtable.. */
+	pg_atomic_uint32 nrels;
+} RepackedRels;
+
+static RepackedRels *repackedRels = NULL;
 
 /*
  * Maximum number of entries in the hashtable.
@@ -2217,22 +2226,38 @@ static HTAB *RepackedRelsHash = NULL;
 Size
 RepackShmemSize(void)
 {
-	return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+	Size		result;
+
+	result = sizeof(RepackedRels);
+
+	result += hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+	return result;
 }
 
 void
 RepackShmemInit(void)
 {
+	bool		found;
 	HASHCTL		info;
 
+	repackedRels = ShmemInitStruct("Repacked Relations",
+								   sizeof(RepackedRels),
+								   &found);
+	if (!IsUnderPostmaster)
+	{
+		Assert(!found);
+		pg_atomic_init_u32(&repackedRels->nrels, 0);
+	}
+	else
+		Assert(found);
+
 	info.keysize = sizeof(RepackedRel);
 	info.entrysize = info.keysize;
-
-	RepackedRelsHash = ShmemInitHash("Repacked Relations",
-									 MAX_REPACKED_RELS,
-									 MAX_REPACKED_RELS,
-									 &info,
-									 HASH_ELEM | HASH_BLOBS);
+	repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash",
+											MAX_REPACKED_RELS,
+											MAX_REPACKED_RELS,
+											&info,
+											HASH_ELEM | HASH_BLOBS);
 }
 
 /*
@@ -2267,13 +2292,14 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 			   *entry;
 	bool		found;
 	static bool before_shmem_exit_callback_setup = false;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	relid = RelationGetRelid(rel);
 	index = index_p ? *index_p : NULL;
 
 	/*
-	 * Make sure that we do not leave an entry in RepackedRelsHash if exiting
-	 * due to FATAL.
+	 * Make sure that we do not leave an entry in repackedRels->hashtable if
+	 * exiting due to FATAL.
 	 */
 	if (!before_shmem_exit_callback_setup)
 	{
@@ -2288,7 +2314,7 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 	*entered_p = false;
 	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found);
 	if (found)
 	{
 		/*
@@ -2306,9 +2332,13 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
 				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
 
+	/* Increment the number of relations. */
+	nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1);
+	Assert(nrels < MAX_REPACKED_RELS);
+
 	/*
-	 * Even if anything fails below, the caller has to do cleanup in the
-	 * shared memory.
+	 * Even if the insertion of TOAST relid should fail below, the caller has
+	 * to do cleanup.
 	 */
 	*entered_p = true;
 
@@ -2390,6 +2420,7 @@ end_concurrent_repack(bool error)
 	RepackedRel key;
 	RepackedRel *entry = NULL;
 	Oid			relid = repacked_rel;
+	uint32		nrels PG_USED_FOR_ASSERTS_ONLY;
 
 	/* Remove the relation from the hash if we managed to insert one. */
 	if (OidIsValid(repacked_rel))
@@ -2398,7 +2429,8 @@ end_concurrent_repack(bool error)
 		key.relid = repacked_rel;
 		key.dbid = MyDatabaseId;
 		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
-		entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL);
+		entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE,
+							NULL);
 		LWLockRelease(RepackedRelsLock);
 
 		/*
@@ -2427,6 +2459,10 @@ end_concurrent_repack(bool error)
 		 * cluster_before_shmem_exit_callback().
 		 */
 		repacked_rel = InvalidOid;
+
+		/* Decrement the number of relations. */
+		nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1);
+		Assert(nrels > 0);
 	}
 
 	/*
@@ -2479,6 +2515,8 @@ cluster_before_shmem_exit_callback(int code, Datum arg)
 
 /*
  * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
  */
 bool
 is_concurrent_repack_in_progress(Oid relid)
@@ -2486,18 +2524,40 @@ is_concurrent_repack_in_progress(Oid relid)
 	RepackedRel key,
 			   *entry;
 
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just use the counter.
+	 */
+	if (!OidIsValid(relid))
+	{
+		if (pg_atomic_read_u32(&repackedRels->nrels) > 0)
+			return true;
+		else
+			return false;
+	}
+
+	/* For particular relation we need to search in the hashtable. */
 	memset(&key, 0, sizeof(key));
 	key.relid = relid;
 	key.dbid = MyDatabaseId;
 
 	LWLockAcquire(RepackedRelsLock, LW_SHARED);
 	entry = (RepackedRel *)
-		hash_search(RepackedRelsHash, &key, HASH_FIND, NULL);
+		hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL);
 	LWLockRelease(RepackedRelsLock);
 
 	return entry != NULL;
 }
 
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
+}
+
 /*
  * This function is much like pg_create_logical_replication_slot() except that
  * the new slot is neither released (if anyone else could read changes from
@@ -2525,8 +2585,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
-	 * regarding logical decoding, treated like a catalog.
+	 * table we are processing is present in repackedRels->hashtable and
+	 * therefore, regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
 									NIL,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index a8d2e024d34..4909432d585 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 5acb4508f85..413bcc1addb 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1313,13 +1313,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index be283c70fce..0267357a261 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -172,6 +172,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 extern Size RepackShmemSize(void);
 extern void RepackShmemInit(void);
 extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
 
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 #endif							/* CLUSTER_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 372065fc570..fcbad5c1720 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -710,12 +710,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1e3e619c4b..b3be8572132 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2511,6 +2511,7 @@ ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
 RepackedRel
+RepackedRels
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.43.5


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v12-0009-Call-logical_rewrite_heap_tuple-when-applying-concur.patch



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

* [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-04-11 09:13  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-04-11 09:13 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 doc/src/sgml/ref/repack.sgml                  |   7 -
 src/backend/access/transam/parallel.c         |   8 +
 src/backend/access/transam/xact.c             | 106 ++++-
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 387 +++++++++++++++++-
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/ipc/standby.c             |   4 +-
 src/backend/utils/cache/inval.c               |  21 +
 src/backend/utils/cache/relcache.c            |   4 +
 src/include/access/xlog.h                     |  15 +-
 src/include/commands/cluster.h                |   5 +
 src/include/utils/inval.h                     |   2 +
 src/include/utils/rel.h                       |   9 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 18 files changed, 540 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 7e44fa636ac..28adb21738a 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -263,13 +263,6 @@ INDEX <replaceable class="parameter">index_name</replaceable> ]
         </para>
        </listitem>
 
-       <listitem>
-        <para>
-          The <link linkend="guc-wal-level"><varname>wal_level</varname></link>
-          configuration parameter is less than <literal>logical</literal>.
-        </para>
-       </listitem>
-
        <listitem>
         <para>
          The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 23f2de587a1..be568f70961 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -126,6 +127,12 @@ static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
 static int	nParallelCurrentXids = 0;
 static TransactionId *ParallelCurrentXids;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -638,6 +645,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -652,6 +660,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -681,20 +715,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -719,6 +739,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2216,6 +2284,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index ec40c0b7c42..b4e07104083 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index cc765b88d52..6e1cdc7bca6 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -84,6 +84,14 @@ typedef struct
  * The following definitions are used for concurrent processing.
  */
 
+/*
+ * OID of the table being repacked by this backend.
+ */
+static Oid	repacked_rel = InvalidOid;
+
+/* The same for its TOAST relation. */
+static Oid	repacked_rel_toast = InvalidOid;
+
 /*
  * The locators are used to avoid logical decoding of data that we do not need
  * for our table.
@@ -135,8 +143,10 @@ static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
 											   ClusterCommand cmd);
 static bool cluster_is_permitted_for_relation(Oid relid, Oid userid,
 											  ClusterCommand cmd);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
+static void begin_concurrent_repack(Relation rel, Relation *index_p,
+									bool *entered_p);
+static void end_concurrent_repack(bool error);
+static void cluster_before_shmem_exit_callback(int code, Datum arg);
 static LogicalDecodingContext *setup_logical_decoding(Oid relid,
 													  const char *slotname,
 													  TupleDesc tupdesc);
@@ -383,6 +393,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 	Relation	index;
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	LOCKMODE	lmode;
+	bool		entered,
+				success;
 
 	/*
 	 * Check that the correct lock is held. The lock mode is
@@ -558,23 +570,31 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 		TransferPredicateLocksToHeapRelation(OldHeap);
 
 	/* rebuild_relation does all the dirty work */
+	entered = false;
+	success = false;
 	PG_TRY();
 	{
 		/*
-		 * For concurrent processing, make sure that our logical decoding
-		 * ignores data changes of other tables than the one we are
-		 * processing.
+		 * For concurrent processing, make sure that
+		 *
+		 * 1) our logical decoding ignores data changes of other tables than
+		 * the one we are processing.
+		 *
+		 * 2) other transactions know that REPACK CONCURRENTLY is in progress
+		 * for our table, so they write sufficient information to WAL even if
+		 * wal_level is < LOGICAL.
 		 */
 		if (concurrent)
-			begin_concurrent_repack(OldHeap);
+			begin_concurrent_repack(OldHeap, &index, &entered);
 
 		rebuild_relation(OldHeap, index, verbose, concurrent, save_userid,
 						 cmd);
+		success = true;
 	}
 	PG_FINALLY();
 	{
-		if (concurrent)
-			end_concurrent_repack();
+		if (concurrent && entered)
+			end_concurrent_repack(!success);
 	}
 	PG_END_TRY();
 
@@ -2207,6 +2227,49 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
 
 #define REPL_PLUGIN_NAME	"pgoutput_repack"
 
+/*
+ * Each relation being processed by REPACK CONCURRENTLY must be in the
+ * repackedRelsHash hashtable.
+ */
+typedef struct RepackedRel
+{
+	Oid			relid;
+	Oid			dbid;
+} RepackedRel;
+
+/* Hashtable of RepackedRel elements. */
+static HTAB	   *repackedRelsHash = NULL;;
+
+/*
+ * Maximum number of entries in the hashtable.
+ *
+ * A replication slot is needed for the processing, so use this GUC to
+ * allocate memory for the hashtable. Multiply by two because TOAST relations
+ * also need to be added to the hashtable.
+ */
+#define	MAX_REPACKED_RELS	(max_replication_slots * 2)
+
+Size
+RepackShmemSize(void)
+{
+	return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+}
+
+void
+RepackShmemInit(void)
+{
+	HASHCTL		info;
+
+	info.keysize = sizeof(RepackedRel);
+	info.entrysize = info.keysize;
+	repackedRelsHash = ShmemInitHash("Repacked Relations Hash",
+									 MAX_REPACKED_RELS,
+									 MAX_REPACKED_RELS,
+									 &info,
+									 HASH_ELEM | HASH_BLOBS |
+									 HASH_FIXED_SIZE);
+}
+
 /*
  * Call this function before REPACK CONCURRENTLY starts to setup logical
  * decoding. It makes sure that other users of the table put enough
@@ -2221,11 +2284,150 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
  *
  * Note that TOAST table needs no attention here as it's not scanned using
  * historic snapshot.
+ *
+ * 'index_p' is in/out argument because the function unlocks the index
+ * temporarily.
+ *
+ * 'enter_p' receives a bool value telling whether relation OID was entered
+ * into repackedRelsHash or not.
  */
 static void
-begin_concurrent_repack(Relation rel)
+begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 {
-	Oid			toastrelid;
+	Oid			relid,
+				toastrelid;
+	Relation	index = NULL;
+	Oid			indexid = InvalidOid;
+	RepackedRel key,
+			   *entry;
+	bool		found;
+	static bool before_shmem_exit_callback_setup = false;
+
+	relid = RelationGetRelid(rel);
+	index = index_p ? *index_p : NULL;
+
+	/*
+	 * Make sure that we do not leave an entry in repackedRelsHash if exiting
+	 * due to FATAL.
+	 */
+	if (!before_shmem_exit_callback_setup)
+	{
+		before_shmem_exit(cluster_before_shmem_exit_callback, 0);
+		before_shmem_exit_callback_setup = true;
+	}
+
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	*entered_p = false;
+	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+	if (found)
+	{
+		/*
+		 * Since REPACK CONCURRENTLY takes ShareRowExclusiveLock, a conflict
+		 * should occur much earlier. However that lock may be released
+		 * temporarily, see below.  Anyway, we should complain whatever the
+		 * reason of the conflict might be.
+		 */
+		ereport(ERROR,
+				(errmsg("relation \"%s\" is already being processed by REPACK CONCURRENTLY",
+						RelationGetRelationName(rel))));
+	}
+	if (entry == NULL)
+		ereport(ERROR,
+				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+	/*
+	 * Even if the insertion of TOAST relid should fail below, the caller has
+	 * to do cleanup.
+	 */
+	*entered_p = true;
+
+	/*
+	 * Enable the callback to remove the entry in case of exit. We should not
+	 * do this earlier, otherwise an attempt to insert already existing entry
+	 * could make us remove that entry (inserted by another backend) during
+	 * ERROR handling.
+	 */
+	Assert(!OidIsValid(repacked_rel));
+	repacked_rel = relid;
+
+	/*
+	 * TOAST relation is not accessed using historic snapshot, but we enter it
+	 * here to protect it from being VACUUMed by another backend. (Lock does
+	 * not help in the CONCURRENTLY case because cannot hold it continuously
+	 * till the end of the transaction.) See the comments on locking TOAST
+	 * relation in copy_table_data().
+	 */
+	toastrelid = rel->rd_rel->reltoastrelid;
+	if (OidIsValid(toastrelid))
+	{
+		key.relid = toastrelid;
+		entry = (RepackedRel *)
+			hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		if (found)
+
+			/*
+			 * If we could enter the main fork the TOAST should succeed too.
+			 * Nevertheless, check.
+			 */
+			ereport(ERROR,
+					(errmsg("TOAST relation of \"%s\" is already being processed by REPACK CONCURRENTLY",
+							RelationGetRelationName(rel))));
+		if (entry == NULL)
+			ereport(ERROR,
+					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+		Assert(!OidIsValid(repacked_rel_toast));
+		repacked_rel_toast = toastrelid;
+	}
+
+	LWLockRelease(RepackedRelsLock);
+
+	/*
+	 * Make sure that other backends are aware of the new hash entry as soon
+	 * as they open our table.
+	 */
+	CacheInvalidateRelcacheImmediate(relid);
+
+	/*
+	 * Also make sure that the existing users of the table update their
+	 * relcache entry as soon as they try to run DML commands on it.
+	 *
+	 * ShareLock is the weakest lock that conflicts with DMLs. If any backend
+	 * has a lower lock, we assume it'll accept our invalidation message when
+	 * it changes the lock mode.
+	 *
+	 * Before upgrading the lock on the relation, close the index temporarily
+	 * to avoid a deadlock if another backend running DML already has its lock
+	 * (ShareLock) on the table and waits for the lock on the index.
+	 */
+	if (index)
+	{
+		indexid = RelationGetRelid(index);
+		index_close(index, ShareUpdateExclusiveLock);
+	}
+	LockRelationOid(relid, ShareLock);
+	UnlockRelationOid(relid, ShareLock);
+	if (OidIsValid(indexid))
+	{
+		/*
+		 * Re-open the index and check that it hasn't changed while unlocked.
+		 */
+		check_index_is_clusterable(rel, indexid, ShareUpdateExclusiveLock);
+
+		/*
+		 * Return the new relcache entry to the caller. (It's been locked by
+		 * the call above.)
+		 */
+		index = index_open(indexid, NoLock);
+		*index_p = index;
+	}
 
 	/* Avoid logical decoding of other relations by this backend. */
 	repacked_rel_locator = rel->rd_locator;
@@ -2243,15 +2445,176 @@ begin_concurrent_repack(Relation rel)
 
 /*
  * Call this when done with REPACK CONCURRENTLY.
+ *
+ * 'error' tells whether the function is being called in order to handle
+ * error.
  */
 static void
-end_concurrent_repack(void)
+end_concurrent_repack(bool error)
 {
+	RepackedRel key;
+	RepackedRel *entry = NULL;
+	RepackedRel *entry_toast = NULL;
+	Oid			relid = repacked_rel;
+	Oid			toastrelid = repacked_rel_toast;
+
+	/* Remove the relation from the hash if we managed to insert one. */
+	if (OidIsValid(repacked_rel))
+	{
+		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+
+		memset(&key, 0, sizeof(key));
+		key.relid = repacked_rel;
+		key.dbid = MyDatabaseId;
+
+		entry = hash_search(repackedRelsHash, &key, HASH_REMOVE, NULL);
+
+		/* Remove the TOAST relation if there is one. */
+		if (OidIsValid(repacked_rel_toast))
+		{
+			key.relid = repacked_rel_toast;
+			entry_toast = hash_search(repackedRelsHash, &key, HASH_REMOVE,
+									  NULL);
+		}
+
+		LWLockRelease(RepackedRelsLock);
+
+		/*
+		 * Make others refresh their information whether they should still
+		 * treat the table as catalog from the perspective of writing WAL.
+		 *
+		 * XXX Unlike entering the entry into the hashtable, we do not bother
+		 * with locking and unlocking the table here:
+		 *
+		 * 1) On normal completion (and sometimes even on ERROR), the caller
+		 * is already holding AccessExclusiveLock on the table, so there
+		 * should be no relcache reference unaware of this change.
+		 *
+		 * 2) In the other cases, the worst scenario is that the other
+		 * backends will write unnecessary information to WAL until they close
+		 * the relation.
+		 *
+		 * Should we use ShareLock mode to fix 2) at least for the non-FATAL
+		 * errors? (Our before_shmem_exit callback is in charge of FATAL, and
+		 * that probably should not try to acquire any lock.)
+		 */
+		CacheInvalidateRelcacheImmediate(repacked_rel);
+
+		/*
+		 * By clearing repacked_rel we also disable
+		 * cluster_before_shmem_exit_callback().
+		 */
+		repacked_rel = InvalidOid;
+		repacked_rel_toast = InvalidOid;
+	}
+
 	/*
 	 * Restore normal function of (future) logical decoding for this backend.
 	 */
 	repacked_rel_locator.relNumber = InvalidOid;
 	repacked_rel_toast_locator.relNumber = InvalidOid;
+
+	/*
+	 * On normal completion (!error), we should not really fail to remove the
+	 * entry. But if it wasn't there for any reason, raise ERROR to make sure
+	 * the transaction is aborted: if other transactions, while changing the
+	 * contents of the relation, didn't know that REPACK CONCURRENTLY was in
+	 * progress, they could have missed to WAL enough information, and thus we
+	 * could have produced an inconsistent table contents.
+	 *
+	 * On the other hand, if we are already handling an error, there's no
+	 * reason to worry about inconsistent contents of the new storage because
+	 * the transaction is going to be rolled back anyway. Furthermore, by
+	 * raising ERROR here we'd shadow the original error.
+	 */
+	if (!error)
+	{
+		char	   *relname;
+
+		if (OidIsValid(relid) && entry == NULL)
+		{
+			relname = get_rel_name(relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+		/*
+		 * Likewise, the TOAST relation should not have disappeared.
+		 */
+		if (OidIsValid(toastrelid) && entry_toast == NULL)
+		{
+			relname = get_rel_name(key.relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								key.relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+	}
+}
+
+/*
+ * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ */
+static void
+cluster_before_shmem_exit_callback(int code, Datum arg)
+{
+	if (OidIsValid(repacked_rel))
+		end_concurrent_repack(true);
+}
+
+/*
+ * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
+ */
+bool
+is_concurrent_repack_in_progress(Oid relid)
+{
+	RepackedRel key,
+			   *entry;
+
+	/* For particular relation we need to search in the hashtable. */
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	LWLockAcquire(RepackedRelsLock, LW_SHARED);
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just check the number of entries.
+	 */
+	if (!OidIsValid(relid))
+	{
+		long	n = hash_get_num_entries(repackedRelsHash);
+
+		LWLockRelease(RepackedRelsLock);
+		return n > 0;
+	}
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_FIND, NULL);
+	LWLockRelease(RepackedRelsLock);
+
+	return entry != NULL;
+}
+
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
 }
 
 /*
@@ -2281,7 +2644,7 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
+	 * table we are processing is present in repackedRelsHash and therefore,
 	 * regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index a8d2e024d34..4909432d585 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index f247e4e7c16..7b27068c338 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -153,6 +153,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
 	size = add_size(size, MemoryContextReportingShmemSize());
+	size = add_size(size, RepackShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -347,6 +348,7 @@ CreateOrAttachShmemStructs(void)
 	InjectionPointShmemInit();
 	AioShmemInit();
 	MemoryContextReportingShmemInit();
+	RepackShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..ab30d448d42 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1325,13 +1325,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 4eb67720737..14eda1c24ee 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -1633,6 +1633,27 @@ CacheInvalidateRelcache(Relation relation)
 								 databaseId, relationId);
 }
 
+/*
+ * CacheInvalidateRelcacheImmediate
+ *		Send invalidation message for the specified relation's relcache entry.
+ *
+ * Currently this is used in REPACK CONCURRENTLY, to make sure that other
+ * backends are aware that the command is being executed for the relation.
+ */
+void
+CacheInvalidateRelcacheImmediate(Oid relid)
+{
+	SharedInvalidationMessage msg;
+
+	msg.rc.id = SHAREDINVALRELCACHE_ID;
+	msg.rc.dbId = MyDatabaseId;
+	msg.rc.relId = relid;
+	/* check AddCatcacheInvalidationMessage() for an explanation */
+	VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
+
+	SendSharedInvalidMessages(&msg, 1);
+}
+
 /*
  * CacheInvalidateRelcacheAll
  *		Register invalidation of the whole relcache at the end of command.
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 75434e32198..058c95bc847 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1279,6 +1279,10 @@ retry:
 	/* make sure relation is marked as having no open file yet */
 	relation->rd_smgr = NULL;
 
+	/* Is REPACK CONCURRENTLY in progress? */
+	relation->rd_repack_concurrent =
+		is_concurrent_repack_in_progress(targetRelId);
+
 	/*
 	 * now we can free the memory allocated for pg_class_tuple
 	 */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 4914f217267..9d5a30d0689 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -150,5 +150,10 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 							 MultiXactId cutoffMulti,
 							 char newrelpersistence);
 
+extern Size RepackShmemSize(void);
+extern void RepackShmemInit(void);
+extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
+
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 #endif							/* CLUSTER_H */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 9b871caef62..ae9dee394dc 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -50,6 +50,8 @@ extern void CacheInvalidateCatalog(Oid catalogId);
 
 extern void CacheInvalidateRelcache(Relation relation);
 
+extern void CacheInvalidateRelcacheImmediate(Oid relid);
+
 extern void CacheInvalidateRelcacheAll(void);
 
 extern void CacheInvalidateRelcacheByTuple(HeapTuple classTuple);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f..cc84592eb1f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -253,6 +253,9 @@ typedef struct RelationData
 	bool		pgstat_enabled; /* should relation stats be counted */
 	/* use "struct" here to avoid needing to include pgstat.h: */
 	struct PgStat_TableStatus *pgstat_info; /* statistics collection area */
+
+	/* Is REPACK CONCURRENTLY being performed on this relation? */
+	bool		rd_repack_concurrent;
 } RelationData;
 
 
@@ -708,12 +711,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6bbc8b419f8..f885eb6d28b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2529,6 +2529,7 @@ ReorderBufferTupleCidKey
 ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
+RepackedRel
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.43.5


--=-=-=--





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

* [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-06-09 10:00  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-06-09 10:00 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 doc/src/sgml/ref/repack.sgml                  |   7 -
 src/backend/access/transam/parallel.c         |   8 +
 src/backend/access/transam/xact.c             | 106 ++++-
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 387 +++++++++++++++++-
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/ipc/standby.c             |   4 +-
 src/backend/utils/cache/inval.c               |  21 +
 src/backend/utils/cache/relcache.c            |   4 +
 src/include/access/xlog.h                     |  15 +-
 src/include/commands/cluster.h                |   5 +
 src/include/utils/inval.h                     |   2 +
 src/include/utils/rel.h                       |   9 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 18 files changed, 540 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index e1313f40599..0fd767eef98 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -260,13 +260,6 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] CONCU
         </para>
        </listitem>
 
-       <listitem>
-        <para>
-          The <link linkend="guc-wal-level"><varname>wal_level</varname></link>
-          configuration parameter is less than <literal>logical</literal>.
-        </para>
-       </listitem>
-
        <listitem>
         <para>
          The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 23f2de587a1..be568f70961 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -126,6 +127,12 @@ static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
 static int	nParallelCurrentXids = 0;
 static TransactionId *ParallelCurrentXids;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -638,6 +645,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -652,6 +660,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -681,20 +715,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -719,6 +739,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2216,6 +2284,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1914859b2ee..f9c0e947ba4 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 0ebc7eacad9..be383a27712 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -84,6 +84,14 @@ typedef struct
  * The following definitions are used for concurrent processing.
  */
 
+/*
+ * OID of the table being repacked by this backend.
+ */
+static Oid	repacked_rel = InvalidOid;
+
+/* The same for its TOAST relation. */
+static Oid	repacked_rel_toast = InvalidOid;
+
 /*
  * The locators are used to avoid logical decoding of data that we do not need
  * for our table.
@@ -135,8 +143,10 @@ static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
 											   ClusterCommand cmd);
 static bool cluster_is_permitted_for_relation(Oid relid, Oid userid,
 											  ClusterCommand cmd);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
+static void begin_concurrent_repack(Relation rel, Relation *index_p,
+									bool *entered_p);
+static void end_concurrent_repack(bool error);
+static void cluster_before_shmem_exit_callback(int code, Datum arg);
 static LogicalDecodingContext *setup_logical_decoding(Oid relid,
 													  const char *slotname,
 													  TupleDesc tupdesc);
@@ -383,6 +393,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 	Relation	index;
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	LOCKMODE	lmode;
+	bool		entered,
+				success;
 
 	/*
 	 * Check that the correct lock is held. The lock mode is
@@ -558,23 +570,31 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 		TransferPredicateLocksToHeapRelation(OldHeap);
 
 	/* rebuild_relation does all the dirty work */
+	entered = false;
+	success = false;
 	PG_TRY();
 	{
 		/*
-		 * For concurrent processing, make sure that our logical decoding
-		 * ignores data changes of other tables than the one we are
-		 * processing.
+		 * For concurrent processing, make sure that
+		 *
+		 * 1) our logical decoding ignores data changes of other tables than
+		 * the one we are processing.
+		 *
+		 * 2) other transactions know that REPACK CONCURRENTLY is in progress
+		 * for our table, so they write sufficient information to WAL even if
+		 * wal_level is < LOGICAL.
 		 */
 		if (concurrent)
-			begin_concurrent_repack(OldHeap);
+			begin_concurrent_repack(OldHeap, &index, &entered);
 
 		rebuild_relation(OldHeap, index, verbose, concurrent, save_userid,
 						 cmd);
+		success = true;
 	}
 	PG_FINALLY();
 	{
-		if (concurrent)
-			end_concurrent_repack();
+		if (concurrent && entered)
+			end_concurrent_repack(!success);
 	}
 	PG_END_TRY();
 
@@ -2208,6 +2228,49 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
 
 #define REPL_PLUGIN_NAME	"pgoutput_repack"
 
+/*
+ * Each relation being processed by REPACK CONCURRENTLY must be in the
+ * repackedRelsHash hashtable.
+ */
+typedef struct RepackedRel
+{
+	Oid			relid;
+	Oid			dbid;
+} RepackedRel;
+
+/* Hashtable of RepackedRel elements. */
+static HTAB	   *repackedRelsHash = NULL;;
+
+/*
+ * Maximum number of entries in the hashtable.
+ *
+ * A replication slot is needed for the processing, so use this GUC to
+ * allocate memory for the hashtable. Multiply by two because TOAST relations
+ * also need to be added to the hashtable.
+ */
+#define	MAX_REPACKED_RELS	(max_replication_slots * 2)
+
+Size
+RepackShmemSize(void)
+{
+	return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+}
+
+void
+RepackShmemInit(void)
+{
+	HASHCTL		info;
+
+	info.keysize = sizeof(RepackedRel);
+	info.entrysize = info.keysize;
+	repackedRelsHash = ShmemInitHash("Repacked Relations Hash",
+									 MAX_REPACKED_RELS,
+									 MAX_REPACKED_RELS,
+									 &info,
+									 HASH_ELEM | HASH_BLOBS |
+									 HASH_FIXED_SIZE);
+}
+
 /*
  * Call this function before REPACK CONCURRENTLY starts to setup logical
  * decoding. It makes sure that other users of the table put enough
@@ -2222,11 +2285,150 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
  *
  * Note that TOAST table needs no attention here as it's not scanned using
  * historic snapshot.
+ *
+ * 'index_p' is in/out argument because the function unlocks the index
+ * temporarily.
+ *
+ * 'enter_p' receives a bool value telling whether relation OID was entered
+ * into repackedRelsHash or not.
  */
 static void
-begin_concurrent_repack(Relation rel)
+begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 {
-	Oid			toastrelid;
+	Oid			relid,
+				toastrelid;
+	Relation	index = NULL;
+	Oid			indexid = InvalidOid;
+	RepackedRel key,
+			   *entry;
+	bool		found;
+	static bool before_shmem_exit_callback_setup = false;
+
+	relid = RelationGetRelid(rel);
+	index = index_p ? *index_p : NULL;
+
+	/*
+	 * Make sure that we do not leave an entry in repackedRelsHash if exiting
+	 * due to FATAL.
+	 */
+	if (!before_shmem_exit_callback_setup)
+	{
+		before_shmem_exit(cluster_before_shmem_exit_callback, 0);
+		before_shmem_exit_callback_setup = true;
+	}
+
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	*entered_p = false;
+	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+	if (found)
+	{
+		/*
+		 * Since REPACK CONCURRENTLY takes ShareRowExclusiveLock, a conflict
+		 * should occur much earlier. However that lock may be released
+		 * temporarily, see below.  Anyway, we should complain whatever the
+		 * reason of the conflict might be.
+		 */
+		ereport(ERROR,
+				(errmsg("relation \"%s\" is already being processed by REPACK CONCURRENTLY",
+						RelationGetRelationName(rel))));
+	}
+	if (entry == NULL)
+		ereport(ERROR,
+				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+	/*
+	 * Even if the insertion of TOAST relid should fail below, the caller has
+	 * to do cleanup.
+	 */
+	*entered_p = true;
+
+	/*
+	 * Enable the callback to remove the entry in case of exit. We should not
+	 * do this earlier, otherwise an attempt to insert already existing entry
+	 * could make us remove that entry (inserted by another backend) during
+	 * ERROR handling.
+	 */
+	Assert(!OidIsValid(repacked_rel));
+	repacked_rel = relid;
+
+	/*
+	 * TOAST relation is not accessed using historic snapshot, but we enter it
+	 * here to protect it from being VACUUMed by another backend. (Lock does
+	 * not help in the CONCURRENTLY case because cannot hold it continuously
+	 * till the end of the transaction.) See the comments on locking TOAST
+	 * relation in copy_table_data().
+	 */
+	toastrelid = rel->rd_rel->reltoastrelid;
+	if (OidIsValid(toastrelid))
+	{
+		key.relid = toastrelid;
+		entry = (RepackedRel *)
+			hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		if (found)
+
+			/*
+			 * If we could enter the main fork the TOAST should succeed too.
+			 * Nevertheless, check.
+			 */
+			ereport(ERROR,
+					(errmsg("TOAST relation of \"%s\" is already being processed by REPACK CONCURRENTLY",
+							RelationGetRelationName(rel))));
+		if (entry == NULL)
+			ereport(ERROR,
+					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+		Assert(!OidIsValid(repacked_rel_toast));
+		repacked_rel_toast = toastrelid;
+	}
+
+	LWLockRelease(RepackedRelsLock);
+
+	/*
+	 * Make sure that other backends are aware of the new hash entry as soon
+	 * as they open our table.
+	 */
+	CacheInvalidateRelcacheImmediate(relid);
+
+	/*
+	 * Also make sure that the existing users of the table update their
+	 * relcache entry as soon as they try to run DML commands on it.
+	 *
+	 * ShareLock is the weakest lock that conflicts with DMLs. If any backend
+	 * has a lower lock, we assume it'll accept our invalidation message when
+	 * it changes the lock mode.
+	 *
+	 * Before upgrading the lock on the relation, close the index temporarily
+	 * to avoid a deadlock if another backend running DML already has its lock
+	 * (ShareLock) on the table and waits for the lock on the index.
+	 */
+	if (index)
+	{
+		indexid = RelationGetRelid(index);
+		index_close(index, ShareUpdateExclusiveLock);
+	}
+	LockRelationOid(relid, ShareLock);
+	UnlockRelationOid(relid, ShareLock);
+	if (OidIsValid(indexid))
+	{
+		/*
+		 * Re-open the index and check that it hasn't changed while unlocked.
+		 */
+		check_index_is_clusterable(rel, indexid, ShareUpdateExclusiveLock);
+
+		/*
+		 * Return the new relcache entry to the caller. (It's been locked by
+		 * the call above.)
+		 */
+		index = index_open(indexid, NoLock);
+		*index_p = index;
+	}
 
 	/* Avoid logical decoding of other relations by this backend. */
 	repacked_rel_locator = rel->rd_locator;
@@ -2244,15 +2446,176 @@ begin_concurrent_repack(Relation rel)
 
 /*
  * Call this when done with REPACK CONCURRENTLY.
+ *
+ * 'error' tells whether the function is being called in order to handle
+ * error.
  */
 static void
-end_concurrent_repack(void)
+end_concurrent_repack(bool error)
 {
+	RepackedRel key;
+	RepackedRel *entry = NULL;
+	RepackedRel *entry_toast = NULL;
+	Oid			relid = repacked_rel;
+	Oid			toastrelid = repacked_rel_toast;
+
+	/* Remove the relation from the hash if we managed to insert one. */
+	if (OidIsValid(repacked_rel))
+	{
+		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+
+		memset(&key, 0, sizeof(key));
+		key.relid = repacked_rel;
+		key.dbid = MyDatabaseId;
+
+		entry = hash_search(repackedRelsHash, &key, HASH_REMOVE, NULL);
+
+		/* Remove the TOAST relation if there is one. */
+		if (OidIsValid(repacked_rel_toast))
+		{
+			key.relid = repacked_rel_toast;
+			entry_toast = hash_search(repackedRelsHash, &key, HASH_REMOVE,
+									  NULL);
+		}
+
+		LWLockRelease(RepackedRelsLock);
+
+		/*
+		 * Make others refresh their information whether they should still
+		 * treat the table as catalog from the perspective of writing WAL.
+		 *
+		 * XXX Unlike entering the entry into the hashtable, we do not bother
+		 * with locking and unlocking the table here:
+		 *
+		 * 1) On normal completion (and sometimes even on ERROR), the caller
+		 * is already holding AccessExclusiveLock on the table, so there
+		 * should be no relcache reference unaware of this change.
+		 *
+		 * 2) In the other cases, the worst scenario is that the other
+		 * backends will write unnecessary information to WAL until they close
+		 * the relation.
+		 *
+		 * Should we use ShareLock mode to fix 2) at least for the non-FATAL
+		 * errors? (Our before_shmem_exit callback is in charge of FATAL, and
+		 * that probably should not try to acquire any lock.)
+		 */
+		CacheInvalidateRelcacheImmediate(repacked_rel);
+
+		/*
+		 * By clearing repacked_rel we also disable
+		 * cluster_before_shmem_exit_callback().
+		 */
+		repacked_rel = InvalidOid;
+		repacked_rel_toast = InvalidOid;
+	}
+
 	/*
 	 * Restore normal function of (future) logical decoding for this backend.
 	 */
 	repacked_rel_locator.relNumber = InvalidOid;
 	repacked_rel_toast_locator.relNumber = InvalidOid;
+
+	/*
+	 * On normal completion (!error), we should not really fail to remove the
+	 * entry. But if it wasn't there for any reason, raise ERROR to make sure
+	 * the transaction is aborted: if other transactions, while changing the
+	 * contents of the relation, didn't know that REPACK CONCURRENTLY was in
+	 * progress, they could have missed to WAL enough information, and thus we
+	 * could have produced an inconsistent table contents.
+	 *
+	 * On the other hand, if we are already handling an error, there's no
+	 * reason to worry about inconsistent contents of the new storage because
+	 * the transaction is going to be rolled back anyway. Furthermore, by
+	 * raising ERROR here we'd shadow the original error.
+	 */
+	if (!error)
+	{
+		char	   *relname;
+
+		if (OidIsValid(relid) && entry == NULL)
+		{
+			relname = get_rel_name(relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+		/*
+		 * Likewise, the TOAST relation should not have disappeared.
+		 */
+		if (OidIsValid(toastrelid) && entry_toast == NULL)
+		{
+			relname = get_rel_name(key.relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								key.relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+	}
+}
+
+/*
+ * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ */
+static void
+cluster_before_shmem_exit_callback(int code, Datum arg)
+{
+	if (OidIsValid(repacked_rel))
+		end_concurrent_repack(true);
+}
+
+/*
+ * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
+ */
+bool
+is_concurrent_repack_in_progress(Oid relid)
+{
+	RepackedRel key,
+			   *entry;
+
+	/* For particular relation we need to search in the hashtable. */
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	LWLockAcquire(RepackedRelsLock, LW_SHARED);
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just check the number of entries.
+	 */
+	if (!OidIsValid(relid))
+	{
+		long	n = hash_get_num_entries(repackedRelsHash);
+
+		LWLockRelease(RepackedRelsLock);
+		return n > 0;
+	}
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_FIND, NULL);
+	LWLockRelease(RepackedRelsLock);
+
+	return entry != NULL;
+}
+
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
 }
 
 /*
@@ -2282,7 +2645,7 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
+	 * table we are processing is present in repackedRelsHash and therefore,
 	 * regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 1d56d0c4ef3..2cde79c9ed4 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index e9ddf39500c..e24e1795aa9 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -151,6 +151,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, InjectionPointShmemSize());
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
+	size = add_size(size, RepackShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -344,6 +345,7 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+	RepackShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..ab30d448d42 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1325,13 +1325,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 02505c88b8e..ecaa2283c2a 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -1643,6 +1643,27 @@ CacheInvalidateRelcache(Relation relation)
 								 databaseId, relationId);
 }
 
+/*
+ * CacheInvalidateRelcacheImmediate
+ *		Send invalidation message for the specified relation's relcache entry.
+ *
+ * Currently this is used in REPACK CONCURRENTLY, to make sure that other
+ * backends are aware that the command is being executed for the relation.
+ */
+void
+CacheInvalidateRelcacheImmediate(Oid relid)
+{
+	SharedInvalidationMessage msg;
+
+	msg.rc.id = SHAREDINVALRELCACHE_ID;
+	msg.rc.dbId = MyDatabaseId;
+	msg.rc.relId = relid;
+	/* check AddCatcacheInvalidationMessage() for an explanation */
+	VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
+
+	SendSharedInvalidMessages(&msg, 1);
+}
+
 /*
  * CacheInvalidateRelcacheAll
  *		Register invalidation of the whole relcache at the end of command.
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 4911642fb3c..504cb8e56a8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1279,6 +1279,10 @@ retry:
 	/* make sure relation is marked as having no open file yet */
 	relation->rd_smgr = NULL;
 
+	/* Is REPACK CONCURRENTLY in progress? */
+	relation->rd_repack_concurrent =
+		is_concurrent_repack_in_progress(targetRelId);
+
 	/*
 	 * now we can free the memory allocated for pg_class_tuple
 	 */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 4914f217267..9d5a30d0689 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -150,5 +150,10 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 							 MultiXactId cutoffMulti,
 							 char newrelpersistence);
 
+extern Size RepackShmemSize(void);
+extern void RepackShmemInit(void);
+extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
+
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 #endif							/* CLUSTER_H */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 9b871caef62..ae9dee394dc 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -50,6 +50,8 @@ extern void CacheInvalidateCatalog(Oid catalogId);
 
 extern void CacheInvalidateRelcache(Relation relation);
 
+extern void CacheInvalidateRelcacheImmediate(Oid relid);
+
 extern void CacheInvalidateRelcacheAll(void);
 
 extern void CacheInvalidateRelcacheByTuple(HeapTuple classTuple);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f..cc84592eb1f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -253,6 +253,9 @@ typedef struct RelationData
 	bool		pgstat_enabled; /* should relation stats be counted */
 	/* use "struct" here to avoid needing to include pgstat.h: */
 	struct PgStat_TableStatus *pgstat_info; /* statistics collection area */
+
+	/* Is REPACK CONCURRENTLY being performed on this relation? */
+	bool		rd_repack_concurrent;
 } RelationData;
 
 
@@ -708,12 +711,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 405d0811b4f..4f6c0ca3a8a 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 0e3c47ba999..716e5619aa7 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -50,9 +50,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 814a0ba7b69..e3be1f42ccf 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2527,6 +2527,7 @@ ReorderBufferTupleCidKey
 ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
+RepackedRel
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.43.5


--=-=-=--





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

* [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY.
@ 2025-06-30 17:41  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Antonin Houska @ 2025-06-30 17:41 UTC (permalink / raw)

As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set
to 'logical', while 'replica' is the default value. If logical replication is
not used, users will probably be reluctant to set the GUC to 'logical' because
it can affect server performance (by writing additional information to WAL)
and because it cannot be changed to 'logical' only for the time REPACK
CONCURRENTLY is running: change of this GUC requires server restart to take
effect.

This patch teaches postgres backend to recognize whether it should consider
wal_level='logical' "locally" for particular transaction, even if the
wal_level GUC is actually set to 'replica'. Also it ensures that the logical
decoding specific information is added to WAL only for the tables which are
currently being processed by REPACK CONCURRENTLY.

If the logical decoding is enabled this way, only temporary replication slots
should be created. The problem of permanent slot is that it is restored during
server restart, and the restore fails if wal_level is not "globally"
'logical'.

There is an independent work in progres to enable logical decoding transiently
[1]. ISTM that this is too "heavyweight" solution for our problem. And I think
that these two approaches are not mutually exclusive: once [1] is committed,
we only need to adjust the XLogLogicalInfoActive() macro.

[1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.g...
---
 doc/src/sgml/ref/repack.sgml                  |   7 -
 src/backend/access/transam/parallel.c         |   8 +
 src/backend/access/transam/xact.c             | 106 ++++-
 src/backend/access/transam/xlog.c             |   1 +
 src/backend/commands/cluster.c                | 387 +++++++++++++++++-
 src/backend/replication/logical/logical.c     |   9 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/ipc/standby.c             |   4 +-
 src/backend/utils/cache/inval.c               |  21 +
 src/backend/utils/cache/relcache.c            |   4 +
 src/include/access/xlog.h                     |  15 +-
 src/include/commands/cluster.h                |   5 +
 src/include/utils/inval.h                     |   2 +
 src/include/utils/rel.h                       |   9 +-
 src/test/modules/injection_points/Makefile    |   1 -
 .../modules/injection_points/logical.conf     |   1 -
 src/test/modules/injection_points/meson.build |   3 -
 src/tools/pgindent/typedefs.list              |   1 +
 18 files changed, 540 insertions(+), 46 deletions(-)
 delete mode 100644 src/test/modules/injection_points/logical.conf

diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index e1313f40599..0fd767eef98 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -260,13 +260,6 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] CONCU
         </para>
        </listitem>
 
-       <listitem>
-        <para>
-          The <link linkend="guc-wal-level"><varname>wal_level</varname></link>
-          configuration parameter is less than <literal>logical</literal>.
-        </para>
-       </listitem>
-
        <listitem>
         <para>
          The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c
index 94db1ec3012..a33318ea7bd 100644
--- a/src/backend/access/transam/parallel.c
+++ b/src/backend/access/transam/parallel.c
@@ -98,6 +98,7 @@ typedef struct FixedParallelState
 	TimestampTz xact_ts;
 	TimestampTz stmt_ts;
 	SerializableXactHandle serializable_xact_handle;
+	int			wal_level_transient;
 
 	/* Mutex protects remaining fields. */
 	slock_t		mutex;
@@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt)
 	fps->xact_ts = GetCurrentTransactionStartTimestamp();
 	fps->stmt_ts = GetCurrentStatementStartTimestamp();
 	fps->serializable_xact_handle = ShareSerializableXact();
+	fps->wal_level_transient = wal_level_transient;
 	SpinLockInit(&fps->mutex);
 	fps->last_xlog_end = 0;
 	shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps);
@@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg)
 	/* Attach to the leader's serializable transaction, if SERIALIZABLE. */
 	AttachSerializableXact(fps->serializable_xact_handle);
 
+	/*
+	 * Restore the information whether this worker should behave as if
+	 * wal_level was WAL_LEVEL_LOGICAL..
+	 */
+	wal_level_transient = fps->wal_level_transient;
+
 	/*
 	 * We've initialized all of our state now; nothing should change
 	 * hereafter.
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 23f2de587a1..be568f70961 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -126,6 +127,12 @@ static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId};
 static int	nParallelCurrentXids = 0;
 static TransactionId *ParallelCurrentXids;
 
+/*
+ * Have we determined the value of wal_level_transient for the current
+ * transaction?
+ */
+static bool wal_level_transient_checked = false;
+
 /*
  * Miscellaneous flag bits to record events which occur on the top level
  * transaction. These flags are only persisted in MyXactFlags and are intended
@@ -638,6 +645,7 @@ AssignTransactionId(TransactionState s)
 	bool		isSubXact = (s->parent != NULL);
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
+	bool		set_wal_level_transient = false;
 
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
@@ -652,6 +660,32 @@ AssignTransactionId(TransactionState s)
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot assign transaction IDs during a parallel operation")));
 
+	/*
+	 * The first call (i.e. the first write) in the transaction tree
+	 * determines whether the whole transaction assumes logical decoding or
+	 * not.
+	 */
+	if (!wal_level_transient_checked)
+	{
+		Assert(wal_level_transient == WAL_LEVEL_MINIMAL);
+
+		/*
+		 * Do not repeat the check when calling this function for parent
+		 * transactions.
+		 */
+		wal_level_transient_checked = true;
+
+		/*
+		 * Remember that the actual check is needed. We cannot do it until the
+		 * top-level transaction has its XID assigned, see comments below.
+		 *
+		 * There is no use case for overriding MINIMAL, and LOGICAL cannot be
+		 * overridden as such.
+		 */
+		if (wal_level == WAL_LEVEL_REPLICA)
+			set_wal_level_transient = true;
+	}
+
 	/*
 	 * Ensure parent(s) have XIDs, so that a child always has an XID later
 	 * than its parent.  Mustn't recurse here, or we might get a stack
@@ -681,20 +715,6 @@ AssignTransactionId(TransactionState s)
 		pfree(parents);
 	}
 
-	/*
-	 * When wal_level=logical, guarantee that a subtransaction's xid can only
-	 * be seen in the WAL stream if its toplevel xid has been logged before.
-	 * If necessary we log an xact_assignment record with fewer than
-	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
-	 * for a transaction even though it appears in a WAL record, we just might
-	 * superfluously log something. That can happen when an xid is included
-	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
-	 * xl_standby_locks.
-	 */
-	if (isSubXact && XLogLogicalInfoActive() &&
-		!TopTransactionStateData.didLogXid)
-		log_unknown_top = true;
-
 	/*
 	 * Generate a new FullTransactionId and record its xid in PGPROC and
 	 * pg_subtrans.
@@ -719,6 +739,54 @@ AssignTransactionId(TransactionState s)
 	if (!isSubXact)
 		RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
 
+	/*
+	 * Check if this transaction should consider wal_level=logical.
+	 *
+	 * Sometimes we need to turn on the logical decoding transiently although
+	 * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table
+	 * is being clustered concurrently, i.e. when we should assume that
+	 * changes done by this transaction will be decoded. In such a case we
+	 * adjust the value of XLogLogicalInfoActive() by setting
+	 * wal_level_transient to LOGICAL.
+	 *
+	 * It's important not to do this check until the XID of the top-level
+	 * transaction is in ProcGlobal: if the decoding becomes mandatory right
+	 * after the check, our transaction will fail to write the necessary
+	 * information to WAL. However, if the top-level transaction is already in
+	 * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts
+	 * record and therefore the snapshot builder will not try to decode the
+	 * transaction (because it assumes it could have missed the initial part
+	 * of the transaction).
+	 *
+	 * On the other hand, if the decoding became mandatory between the actual
+	 * XID assignment and now, the transaction will WAL the decoding specific
+	 * information unnecessarily. Let's assume that such race conditions do
+	 * not happen too often.
+	 */
+	if (set_wal_level_transient)
+	{
+		/*
+		 * Check for the operation that enables the logical decoding
+		 * transiently.
+		 */
+		if (is_concurrent_repack_in_progress(InvalidOid))
+			wal_level_transient = WAL_LEVEL_LOGICAL;
+	}
+
+	/*
+	 * When wal_level=logical, guarantee that a subtransaction's xid can only
+	 * be seen in the WAL stream if its toplevel xid has been logged before.
+	 * If necessary we log an xact_assignment record with fewer than
+	 * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set
+	 * for a transaction even though it appears in a WAL record, we just might
+	 * superfluously log something. That can happen when an xid is included
+	 * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in
+	 * xl_standby_locks.
+	 */
+	if (isSubXact && XLogLogicalInfoActive() &&
+		!TopTransactionStateData.didLogXid)
+		log_unknown_top = true;
+
 	/*
 	 * Acquire lock on the transaction XID.  (We assume this cannot block.) We
 	 * have to ensure that the lock is assigned to the transaction's own
@@ -2216,6 +2284,16 @@ StartTransaction(void)
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
 
+	/*
+	 * wal_level_transient can override wal_level for individual transactions,
+	 * which effectively enables logical decoding for them. At the moment we
+	 * don't know if this transaction will write any data changes to be
+	 * decoded. Should it do, AssignTransactionId() will check if the decoding
+	 * needs to be considered.
+	 */
+	wal_level_transient = WAL_LEVEL_MINIMAL;
+	wal_level_transient_checked = false;
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 47ffc0a2307..dc222db6a5d 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -129,6 +129,7 @@ bool		wal_recycle = true;
 bool		log_checkpoints = true;
 int			wal_sync_method = DEFAULT_WAL_SYNC_METHOD;
 int			wal_level = WAL_LEVEL_REPLICA;
+int			wal_level_transient = WAL_LEVEL_MINIMAL;
 int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 int			wal_retrieve_retry_interval = 5000;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 37f69f369eb..7ecef2b86fc 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -84,6 +84,14 @@ typedef struct
  * The following definitions are used for concurrent processing.
  */
 
+/*
+ * OID of the table being repacked by this backend.
+ */
+static Oid	repacked_rel = InvalidOid;
+
+/* The same for its TOAST relation. */
+static Oid	repacked_rel_toast = InvalidOid;
+
 /*
  * The locators are used to avoid logical decoding of data that we do not need
  * for our table.
@@ -135,8 +143,10 @@ static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
 											   ClusterCommand cmd);
 static bool cluster_is_permitted_for_relation(Oid relid, Oid userid,
 											  ClusterCommand cmd);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
+static void begin_concurrent_repack(Relation rel, Relation *index_p,
+									bool *entered_p);
+static void end_concurrent_repack(bool error);
+static void cluster_before_shmem_exit_callback(int code, Datum arg);
 static LogicalDecodingContext *setup_logical_decoding(Oid relid,
 													  const char *slotname,
 													  TupleDesc tupdesc);
@@ -383,6 +393,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 	Relation	index;
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	LOCKMODE	lmode;
+	bool		entered,
+				success;
 
 	/*
 	 * Check that the correct lock is held. The lock mode is
@@ -558,23 +570,31 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params,
 		TransferPredicateLocksToHeapRelation(OldHeap);
 
 	/* rebuild_relation does all the dirty work */
+	entered = false;
+	success = false;
 	PG_TRY();
 	{
 		/*
-		 * For concurrent processing, make sure that our logical decoding
-		 * ignores data changes of other tables than the one we are
-		 * processing.
+		 * For concurrent processing, make sure that
+		 *
+		 * 1) our logical decoding ignores data changes of other tables than
+		 * the one we are processing.
+		 *
+		 * 2) other transactions know that REPACK CONCURRENTLY is in progress
+		 * for our table, so they write sufficient information to WAL even if
+		 * wal_level is < LOGICAL.
 		 */
 		if (concurrent)
-			begin_concurrent_repack(OldHeap);
+			begin_concurrent_repack(OldHeap, &index, &entered);
 
 		rebuild_relation(OldHeap, index, verbose, concurrent, save_userid,
 						 cmd);
+		success = true;
 	}
 	PG_FINALLY();
 	{
-		if (concurrent)
-			end_concurrent_repack();
+		if (concurrent && entered)
+			end_concurrent_repack(!success);
 	}
 	PG_END_TRY();
 
@@ -2208,6 +2228,49 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
 
 #define REPL_PLUGIN_NAME	"pgoutput_repack"
 
+/*
+ * Each relation being processed by REPACK CONCURRENTLY must be in the
+ * repackedRelsHash hashtable.
+ */
+typedef struct RepackedRel
+{
+	Oid			relid;
+	Oid			dbid;
+} RepackedRel;
+
+/* Hashtable of RepackedRel elements. */
+static HTAB	   *repackedRelsHash = NULL;;
+
+/*
+ * Maximum number of entries in the hashtable.
+ *
+ * A replication slot is needed for the processing, so use this GUC to
+ * allocate memory for the hashtable. Multiply by two because TOAST relations
+ * also need to be added to the hashtable.
+ */
+#define	MAX_REPACKED_RELS	(max_replication_slots * 2)
+
+Size
+RepackShmemSize(void)
+{
+	return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel));
+}
+
+void
+RepackShmemInit(void)
+{
+	HASHCTL		info;
+
+	info.keysize = sizeof(RepackedRel);
+	info.entrysize = info.keysize;
+	repackedRelsHash = ShmemInitHash("Repacked Relations Hash",
+									 MAX_REPACKED_RELS,
+									 MAX_REPACKED_RELS,
+									 &info,
+									 HASH_ELEM | HASH_BLOBS |
+									 HASH_FIXED_SIZE);
+}
+
 /*
  * Call this function before REPACK CONCURRENTLY starts to setup logical
  * decoding. It makes sure that other users of the table put enough
@@ -2222,11 +2285,150 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd)
  *
  * Note that TOAST table needs no attention here as it's not scanned using
  * historic snapshot.
+ *
+ * 'index_p' is in/out argument because the function unlocks the index
+ * temporarily.
+ *
+ * 'enter_p' receives a bool value telling whether relation OID was entered
+ * into repackedRelsHash or not.
  */
 static void
-begin_concurrent_repack(Relation rel)
+begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p)
 {
-	Oid			toastrelid;
+	Oid			relid,
+				toastrelid;
+	Relation	index = NULL;
+	Oid			indexid = InvalidOid;
+	RepackedRel key,
+			   *entry;
+	bool		found;
+	static bool before_shmem_exit_callback_setup = false;
+
+	relid = RelationGetRelid(rel);
+	index = index_p ? *index_p : NULL;
+
+	/*
+	 * Make sure that we do not leave an entry in repackedRelsHash if exiting
+	 * due to FATAL.
+	 */
+	if (!before_shmem_exit_callback_setup)
+	{
+		before_shmem_exit(cluster_before_shmem_exit_callback, 0);
+		before_shmem_exit_callback_setup = true;
+	}
+
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	*entered_p = false;
+	LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+	if (found)
+	{
+		/*
+		 * Since REPACK CONCURRENTLY takes ShareRowExclusiveLock, a conflict
+		 * should occur much earlier. However that lock may be released
+		 * temporarily, see below.  Anyway, we should complain whatever the
+		 * reason of the conflict might be.
+		 */
+		ereport(ERROR,
+				(errmsg("relation \"%s\" is already being processed by REPACK CONCURRENTLY",
+						RelationGetRelationName(rel))));
+	}
+	if (entry == NULL)
+		ereport(ERROR,
+				(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+				(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+	/*
+	 * Even if the insertion of TOAST relid should fail below, the caller has
+	 * to do cleanup.
+	 */
+	*entered_p = true;
+
+	/*
+	 * Enable the callback to remove the entry in case of exit. We should not
+	 * do this earlier, otherwise an attempt to insert already existing entry
+	 * could make us remove that entry (inserted by another backend) during
+	 * ERROR handling.
+	 */
+	Assert(!OidIsValid(repacked_rel));
+	repacked_rel = relid;
+
+	/*
+	 * TOAST relation is not accessed using historic snapshot, but we enter it
+	 * here to protect it from being VACUUMed by another backend. (Lock does
+	 * not help in the CONCURRENTLY case because cannot hold it continuously
+	 * till the end of the transaction.) See the comments on locking TOAST
+	 * relation in copy_table_data().
+	 */
+	toastrelid = rel->rd_rel->reltoastrelid;
+	if (OidIsValid(toastrelid))
+	{
+		key.relid = toastrelid;
+		entry = (RepackedRel *)
+			hash_search(repackedRelsHash, &key, HASH_ENTER_NULL, &found);
+		if (found)
+
+			/*
+			 * If we could enter the main fork the TOAST should succeed too.
+			 * Nevertheless, check.
+			 */
+			ereport(ERROR,
+					(errmsg("TOAST relation of \"%s\" is already being processed by REPACK CONCURRENTLY",
+							RelationGetRelationName(rel))));
+		if (entry == NULL)
+			ereport(ERROR,
+					(errmsg("too many requests for REPACK CONCURRENTLY at a time")),
+					(errhint("Please consider increasing the \"max_replication_slots\" configuration parameter.")));
+
+		Assert(!OidIsValid(repacked_rel_toast));
+		repacked_rel_toast = toastrelid;
+	}
+
+	LWLockRelease(RepackedRelsLock);
+
+	/*
+	 * Make sure that other backends are aware of the new hash entry as soon
+	 * as they open our table.
+	 */
+	CacheInvalidateRelcacheImmediate(relid);
+
+	/*
+	 * Also make sure that the existing users of the table update their
+	 * relcache entry as soon as they try to run DML commands on it.
+	 *
+	 * ShareLock is the weakest lock that conflicts with DMLs. If any backend
+	 * has a lower lock, we assume it'll accept our invalidation message when
+	 * it changes the lock mode.
+	 *
+	 * Before upgrading the lock on the relation, close the index temporarily
+	 * to avoid a deadlock if another backend running DML already has its lock
+	 * (ShareLock) on the table and waits for the lock on the index.
+	 */
+	if (index)
+	{
+		indexid = RelationGetRelid(index);
+		index_close(index, ShareUpdateExclusiveLock);
+	}
+	LockRelationOid(relid, ShareLock);
+	UnlockRelationOid(relid, ShareLock);
+	if (OidIsValid(indexid))
+	{
+		/*
+		 * Re-open the index and check that it hasn't changed while unlocked.
+		 */
+		check_index_is_clusterable(rel, indexid, ShareUpdateExclusiveLock);
+
+		/*
+		 * Return the new relcache entry to the caller. (It's been locked by
+		 * the call above.)
+		 */
+		index = index_open(indexid, NoLock);
+		*index_p = index;
+	}
 
 	/* Avoid logical decoding of other relations by this backend. */
 	repacked_rel_locator = rel->rd_locator;
@@ -2244,15 +2446,176 @@ begin_concurrent_repack(Relation rel)
 
 /*
  * Call this when done with REPACK CONCURRENTLY.
+ *
+ * 'error' tells whether the function is being called in order to handle
+ * error.
  */
 static void
-end_concurrent_repack(void)
+end_concurrent_repack(bool error)
 {
+	RepackedRel key;
+	RepackedRel *entry = NULL;
+	RepackedRel *entry_toast = NULL;
+	Oid			relid = repacked_rel;
+	Oid			toastrelid = repacked_rel_toast;
+
+	/* Remove the relation from the hash if we managed to insert one. */
+	if (OidIsValid(repacked_rel))
+	{
+		LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE);
+
+		memset(&key, 0, sizeof(key));
+		key.relid = repacked_rel;
+		key.dbid = MyDatabaseId;
+
+		entry = hash_search(repackedRelsHash, &key, HASH_REMOVE, NULL);
+
+		/* Remove the TOAST relation if there is one. */
+		if (OidIsValid(repacked_rel_toast))
+		{
+			key.relid = repacked_rel_toast;
+			entry_toast = hash_search(repackedRelsHash, &key, HASH_REMOVE,
+									  NULL);
+		}
+
+		LWLockRelease(RepackedRelsLock);
+
+		/*
+		 * Make others refresh their information whether they should still
+		 * treat the table as catalog from the perspective of writing WAL.
+		 *
+		 * XXX Unlike entering the entry into the hashtable, we do not bother
+		 * with locking and unlocking the table here:
+		 *
+		 * 1) On normal completion (and sometimes even on ERROR), the caller
+		 * is already holding AccessExclusiveLock on the table, so there
+		 * should be no relcache reference unaware of this change.
+		 *
+		 * 2) In the other cases, the worst scenario is that the other
+		 * backends will write unnecessary information to WAL until they close
+		 * the relation.
+		 *
+		 * Should we use ShareLock mode to fix 2) at least for the non-FATAL
+		 * errors? (Our before_shmem_exit callback is in charge of FATAL, and
+		 * that probably should not try to acquire any lock.)
+		 */
+		CacheInvalidateRelcacheImmediate(repacked_rel);
+
+		/*
+		 * By clearing repacked_rel we also disable
+		 * cluster_before_shmem_exit_callback().
+		 */
+		repacked_rel = InvalidOid;
+		repacked_rel_toast = InvalidOid;
+	}
+
 	/*
 	 * Restore normal function of (future) logical decoding for this backend.
 	 */
 	repacked_rel_locator.relNumber = InvalidOid;
 	repacked_rel_toast_locator.relNumber = InvalidOid;
+
+	/*
+	 * On normal completion (!error), we should not really fail to remove the
+	 * entry. But if it wasn't there for any reason, raise ERROR to make sure
+	 * the transaction is aborted: if other transactions, while changing the
+	 * contents of the relation, didn't know that REPACK CONCURRENTLY was in
+	 * progress, they could have missed to WAL enough information, and thus we
+	 * could have produced an inconsistent table contents.
+	 *
+	 * On the other hand, if we are already handling an error, there's no
+	 * reason to worry about inconsistent contents of the new storage because
+	 * the transaction is going to be rolled back anyway. Furthermore, by
+	 * raising ERROR here we'd shadow the original error.
+	 */
+	if (!error)
+	{
+		char	   *relname;
+
+		if (OidIsValid(relid) && entry == NULL)
+		{
+			relname = get_rel_name(relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+		/*
+		 * Likewise, the TOAST relation should not have disappeared.
+		 */
+		if (OidIsValid(toastrelid) && entry_toast == NULL)
+		{
+			relname = get_rel_name(key.relid);
+			if (!relname)
+				ereport(ERROR,
+						(errmsg("cache lookup failed for relation %u",
+								key.relid)));
+
+			ereport(ERROR,
+					(errmsg("relation \"%s\" not found among repacked relations",
+							relname)));
+		}
+
+	}
+}
+
+/*
+ * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback.
+ */
+static void
+cluster_before_shmem_exit_callback(int code, Datum arg)
+{
+	if (OidIsValid(repacked_rel))
+		end_concurrent_repack(true);
+}
+
+/*
+ * Check if relation is currently being processed by REPACK CONCURRENTLY.
+ *
+ * If relid is InvalidOid, check if any relation is being processed.
+ */
+bool
+is_concurrent_repack_in_progress(Oid relid)
+{
+	RepackedRel key,
+			   *entry;
+
+	/* For particular relation we need to search in the hashtable. */
+	memset(&key, 0, sizeof(key));
+	key.relid = relid;
+	key.dbid = MyDatabaseId;
+
+	LWLockAcquire(RepackedRelsLock, LW_SHARED);
+	/*
+	 * If the caller is interested whether any relation is being repacked,
+	 * just check the number of entries.
+	 */
+	if (!OidIsValid(relid))
+	{
+		long	n = hash_get_num_entries(repackedRelsHash);
+
+		LWLockRelease(RepackedRelsLock);
+		return n > 0;
+	}
+	entry = (RepackedRel *)
+		hash_search(repackedRelsHash, &key, HASH_FIND, NULL);
+	LWLockRelease(RepackedRelsLock);
+
+	return entry != NULL;
+}
+
+/*
+ * Is this backend performing REPACK CONCURRENTLY?
+ */
+bool
+is_concurrent_repack_run_by_me(void)
+{
+	return OidIsValid(repacked_rel);
 }
 
 /*
@@ -2282,7 +2645,7 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc)
 	 * useful for us.
 	 *
 	 * Regarding the value of need_full_snapshot, we pass false because the
-	 * table we are processing is present in RepackedRelsHash and therefore,
+	 * table we are processing is present in repackedRelsHash and therefore,
 	 * regarding logical decoding, treated like a catalog.
 	 */
 	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index f1eb798f3e9..5e6000db086 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -31,6 +31,7 @@
 #include "access/xact.h"
 #include "access/xlog_internal.h"
 #include "access/xlogutils.h"
+#include "commands/cluster.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -114,10 +115,12 @@ CheckLogicalDecodingRequirements(void)
 
 	/*
 	 * NB: Adding a new requirement likely means that RestoreSlotFromDisk()
-	 * needs the same check.
+	 * needs the same check. (Except that only temporary slots should be
+	 * created for REPACK CONCURRENTLY, which effectively raises wal_level to
+	 * LOGICAL.)
 	 */
-
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me())
+		|| wal_level < WAL_LEVEL_REPLICA)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("logical decoding requires \"wal_level\" >= \"logical\"")));
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index e9ddf39500c..e24e1795aa9 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -151,6 +151,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, InjectionPointShmemSize());
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
+	size = add_size(size, RepackShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -344,6 +345,7 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+	RepackShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..ab30d448d42 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1325,13 +1325,13 @@ LogStandbySnapshot(void)
 	 * record. Fortunately this routine isn't executed frequently, and it's
 	 * only a shared lock.
 	 */
-	if (wal_level < WAL_LEVEL_LOGICAL)
+	if (!XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	recptr = LogCurrentRunningXacts(running);
 
 	/* Release lock if we kept it longer ... */
-	if (wal_level >= WAL_LEVEL_LOGICAL)
+	if (XLogLogicalInfoActive())
 		LWLockRelease(ProcArrayLock);
 
 	/* GetRunningTransactionData() acquired XidGenLock, we must release it */
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 02505c88b8e..ecaa2283c2a 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -1643,6 +1643,27 @@ CacheInvalidateRelcache(Relation relation)
 								 databaseId, relationId);
 }
 
+/*
+ * CacheInvalidateRelcacheImmediate
+ *		Send invalidation message for the specified relation's relcache entry.
+ *
+ * Currently this is used in REPACK CONCURRENTLY, to make sure that other
+ * backends are aware that the command is being executed for the relation.
+ */
+void
+CacheInvalidateRelcacheImmediate(Oid relid)
+{
+	SharedInvalidationMessage msg;
+
+	msg.rc.id = SHAREDINVALRELCACHE_ID;
+	msg.rc.dbId = MyDatabaseId;
+	msg.rc.relId = relid;
+	/* check AddCatcacheInvalidationMessage() for an explanation */
+	VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
+
+	SendSharedInvalidMessages(&msg, 1);
+}
+
 /*
  * CacheInvalidateRelcacheAll
  *		Register invalidation of the whole relcache at the end of command.
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 4911642fb3c..504cb8e56a8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1279,6 +1279,10 @@ retry:
 	/* make sure relation is marked as having no open file yet */
 	relation->rd_smgr = NULL;
 
+	/* Is REPACK CONCURRENTLY in progress? */
+	relation->rd_repack_concurrent =
+		is_concurrent_repack_in_progress(targetRelId);
+
 	/*
 	 * now we can free the memory allocated for pg_class_tuple
 	 */
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d313099c027..a325bb1d16b 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -95,6 +95,12 @@ typedef enum RecoveryState
 
 extern PGDLLIMPORT int wal_level;
 
+/*
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * enabled transiently.
+ */
+extern PGDLLIMPORT int wal_level_transient;
+
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
 	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
@@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level;
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
 
-/* Do we need to WAL-log information required only for logical replication? */
-#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL)
+/*
+ * Do we need to WAL-log information required only for logical replication?
+ *
+ * wal_level_transient overrides wal_level if logical decoding needs to be
+ * active transiently.
+ */
+#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL)
 
 #ifdef WAL_DEBUG
 extern PGDLLIMPORT bool XLOG_DEBUG;
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 4914f217267..9d5a30d0689 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -150,5 +150,10 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 							 MultiXactId cutoffMulti,
 							 char newrelpersistence);
 
+extern Size RepackShmemSize(void);
+extern void RepackShmemInit(void);
+extern bool is_concurrent_repack_in_progress(Oid relid);
+extern bool is_concurrent_repack_run_by_me(void);
+
 extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 #endif							/* CLUSTER_H */
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 9b871caef62..ae9dee394dc 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -50,6 +50,8 @@ extern void CacheInvalidateCatalog(Oid catalogId);
 
 extern void CacheInvalidateRelcache(Relation relation);
 
+extern void CacheInvalidateRelcacheImmediate(Oid relid);
+
 extern void CacheInvalidateRelcacheAll(void);
 
 extern void CacheInvalidateRelcacheByTuple(HeapTuple classTuple);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f..cc84592eb1f 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -253,6 +253,9 @@ typedef struct RelationData
 	bool		pgstat_enabled; /* should relation stats be counted */
 	/* use "struct" here to avoid needing to include pgstat.h: */
 	struct PgStat_TableStatus *pgstat_info; /* statistics collection area */
+
+	/* Is REPACK CONCURRENTLY being performed on this relation? */
+	bool		rd_repack_concurrent;
 } RelationData;
 
 
@@ -708,12 +711,16 @@ RelationCloseSmgr(Relation relation)
  * it would complicate decoding slightly for little gain). Note that we *do*
  * log information for user defined catalog tables since they presumably are
  * interesting to the user...
+ *
+ * If particular relations require that, the logical decoding can be active
+ * even if wal_level is REPLICA. Do not log other relations in that case.
  */
 #define RelationIsLogicallyLogged(relation) \
 	(XLogLogicalInfoActive() && \
 	 RelationNeedsWAL(relation) && \
 	 (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&	\
-	 !IsCatalogRelation(relation))
+	 !IsCatalogRelation(relation) && \
+	 (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent))
 
 /* routines in utils/cache/relcache.c */
 extern void RelationIncrementReferenceCount(Relation rel);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 30ffe509239..e71b8a19116 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc vacuum
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic inplace syscache-update-pruned repack
-ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 TAP_TESTS = 1
 
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
deleted file mode 100644
index c8f264bc6cb..00000000000
--- a/src/test/modules/injection_points/logical.conf
+++ /dev/null
@@ -1 +0,0 @@
-wal_level = logical
\ No newline at end of file
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index c7daa669548..13c2b627a0b 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -51,9 +51,6 @@ tests += {
       'syscache-update-pruned',
     ],
     'runningcheck': false, # see syscache-update-pruned
-    # 'repack' requires wal_level = 'logical'.
-    'regress_args': ['--temp-config', files('logical.conf')],
-
   },
   'tap': {
     'env': {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 879977ea41f..add58883124 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2532,6 +2532,7 @@ ReorderBufferTupleCidKey
 ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
+RepackedRel
 RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
-- 
2.47.1


--=-=-=--





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


end of thread, other threads:[~2025-06-30 17:41 UTC | newest]

Thread overview: 9+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-12-16 15:26 [PATCH v4 2/2] Make resowners more easily extensible. Heikki Linnakangas <[email protected]>
2025-02-26 08:17 [PATCH v09 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-02-26 08:17 [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-03-24 19:17 [PATCH v10 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Álvaro Herrera <[email protected]>
2025-03-31 13:47 [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-04-01 11:48 [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-04-11 09:13 [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-06-09 10:00 [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[email protected]>
2025-06-30 17:41 [PATCH 7/7] Enable logical decoding transiently, only for REPACK CONCURRENTLY. Antonin Houska <[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