Subject: Memory leak in pg_logical_slot_{get,peek}_changes

From: vignesh C <[email-masked]>
Date: Wed, 11 Dec 2024 09:57:38 +0530

Hi,

I'm starting a new thread for one of the issues reported by Sawada-san at [1].

This is a memory leak on CacheMemoryContext when using pgoutput via SQL APIs:
/* Map must live as long as the session does. */
oldctx = MemoryContextSwitchTo(CacheMemoryContext);

entry->attrmap = build_attrmap_by_name_if_req(indesc, outdesc, false);

MemoryContextSwitchTo(oldctx);
RelationClose(ancestor);

entry->attrmap is pfree'd only when validating the RelationSyncEntry
so remains even after logical decoding API calls.

This issue can be reproduced with the following test:
-- Create table
create table t1( c1 int, c2 int, c3 int) PARTITION BY LIST (c1);
create table t1_1( c2 int, c1 int, c3 int);
ALTER TABLE t1 ATTACH PARTITION t1_1 FOR VALUES IN (0, 1, 2, 3);

-- Create publication
create publication pub1 FOR TABLE t1, t1_1 WITH
(publish_via_partition_root = true);

-- Create replication slot
SELECT 'init' FROM pg_create_logical_replication_slot('test', 'pgoutput');

-- Run the below steps between Test start and Test end many times to
see the memory usage getting increased
-- Test start
insert into t1 values( 1);
SELECT count(*) FROM pg_logical_slot_get_binary_changes('test', NULL,
NULL, 'proto_version', '4', 'publication_names', 'pub1');

-- Memory increases after each invalidation and insert
SELECT * FROM pg_backend_memory_contexts where name = 'CacheMemoryContext';
-- Test end

The attached patch resolves a memory leak by ensuring that the
attribute map is properly freed during plugin shutdown. This process
is triggered by the SQL API when the decoding context is being
released. Additionally, I am conducting a review to identify and
address any similar memory leaks that may exist elsewhere in the code.

[1] - https://www.postgresql.org/message-id/CAD21AoDkAhQVSukOfH3_reuF-j4EU0-HxMqU3dU%2BbSTxsqT14Q%40mail.gmail.com

Regards,
Vignesh
From 36e5c10105d934da0d51474b0ad7c5bd9087e2aa Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Wed, 11 Dec 2024 08:57:06 +0530
Subject: [PATCH v1] Fix memory leak in pgoutput relation attribute map

The pgoutput module caches relation attribute map and frees it upon
invalidation.  However, this was not getting freed incase of SQL functions like
pg_logical_slot_{get,peek}_changes() which would bloat its cache memory usage.
Every pg_logical_slot_{get,peek}_changes() call for changes on partition table
creates more bloat. To address this, this relation attribute map is freed while
the output plugin is shutdown.
---
 src/backend/replication/pgoutput/pgoutput.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b50b3d62e3..f1ef13d313 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1747,6 +1747,16 @@ pgoutput_shutdown(LogicalDecodingContext *ctx)
 {
 	if (RelationSyncCache)
 	{
+		RelationSyncEntry *entry;
+		HASH_SEQ_STATUS status;
+
+		hash_seq_init(&status, RelationSyncCache);
+		while ((entry = (RelationSyncEntry *) hash_seq_search(&status)) != NULL)
+		{
+			if (entry->attrmap)
+				free_attrmap(entry->attrmap);
+		}
+
 		hash_destroy(RelationSyncCache);
 		RelationSyncCache = NULL;
 	}
-- 
2.43.0

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Wed, 11 Dec 2024 06:09:15 +0000

> address any similar memory leaks that may exist elsewhere in the code.

Thanks for reporting the issue and share the fix.

I am not sure if freeing them in shutdown callback is safe, because shutdown
callback Is not invoked in case of ERRORs. I think we'd better allocate them
under cachectx in the beginning to avoid freeing them explicitly.

The attachment is a POC that I internally experimented before. It replaces not
only the attrmap's context, but also others which were allocated under
CacheMemoryContext, to be consistent. Note that I have not tested it deeply.
Feel free to use if it works for you.

But the patch can only be used since PG15 where cachectx is introduced. In
older braches, we may need to think some other ways.

Best Regards,
Hou zj

---

From: vignesh C <[email-masked]>
Date: Wed, 11 Dec 2024 14:41:27 +0530

> under cachectx in the beginning to avoid freeing them explicitly.

I initially considered addressing this issue in a way similar to your
suggestion while fixing it, but later decided to make the change in
pgoutput_shutdown, following the approach used for RelationSyncCache.
This was because RelationSyncCache relies on CacheMemoryContext, and
attrmap is a member of RelationSyncCache entry. Now that we're
considering attrmap in the context of cachectx, do you think we should
apply cachectx to RelationSyncCache as well to solve the similar issue
that can occur with RelationSyncCache.

Regards,
Vignesh

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Wed, 11 Dec 2024 09:45:50 +0000

On Wednesday, December 11, 2024 5:11 PM vignesh C <[email-masked]> wrote:
> 
> On Wed, 11 Dec 2024 at 11:39, Zhijie Hou (Fujitsu) <[email-masked]>
> wrote:
> >
> > I am not sure if freeing them in shutdown callback is safe, because
> > shutdown callback Is not invoked in case of ERRORs. I think we'd
> > better allocate them under cachectx in the beginning to avoid freeing them
> explicitly.
> 
> I initially considered addressing this issue in a way similar to your suggestion
> while fixing it, but later decided to make the change in pgoutput_shutdown,
> following the approach used for RelationSyncCache.
> attrmap is a member of RelationSyncCache entry.

I think we have tended to allocate the member of RelationSyncEntry under
logical decoding context since 52e4f0c. I think that makes more sense because these
members ideally should live as long as the decoding context. In addition, it was
suggested[1] that allocating all the thing under CacheMemoryContext is hard to
debug. And people in another thread[2] also seems agree to remove the dependency
of CacheMemoryContext in the long term.

> Now that we're considering
> attrmap in the context of cachectx, do you think we should apply cachectx to
> RelationSyncCache as well to solve the similar issue that can occur with
> RelationSyncCache.

I personally think it could be considered as a separate project because
RelationSyncCache is accessed even after shutting down the output plugin due to
the registered cache invalidation callbacks. We probably need
MemoryContextRegisterResetCallback() to reset the static pointer
(RelationSyncCache).

[1] https://www.postgresql.org/message-id/20220129003110.6ndrrpanem5sb4ee%40alap3.anarazel.de
[2] https://www.postgresql.org/message-id/Z1d-uR20pt6wtQIS%40paquier.xyz

Best Regards,
Hou zj

---

From: vignesh C <[email-masked]>
Date: Thu, 12 Dec 2024 20:32:15 +0530

> older braches, we may need to think some other ways.

Since we cannot add a new member to PGOutputData, how about creating a
new global static memory context specifically for storing the relation
entry attribute map? This memory context could be allocated and reset
during pgoutput_startup. This way, if a SQL call is made after an
error, the context will be reset, preventing memory bloat as addressed
in the attached patch. Thoughts?

Regards,
Vignesh
From d9c7159903bc9266a0846269c971631f36bb8748 Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Thu, 12 Dec 2024 19:01:32 +0530
Subject: [PATCH vPG14_] Fix memory leak in pgoutput relation attribute map

The pgoutput module caches relation attribute map and frees it upon
invalidation.  However, this was not getting freed incase of SQL functions like
pg_logical_slot_{get,peek}_changes() which would bloat its cache memory usage.
Every pg_logical_slot_{get,peek}_changes() call for changes on partition table
creates more bloat. To address this issue, a new global static memory context
has been introduced, which is reset during pgoutput_startup to avoid
bloat after each pg_logical_slot_{get,peek}_changes() call.
---
 src/backend/replication/pgoutput/pgoutput.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 78c81888c4..ef2e6ab723 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -64,6 +64,7 @@ static void pgoutput_stream_commit(struct LogicalDecodingContext *ctx,
 
 static bool publications_valid;
 static bool in_streaming;
+static MemoryContext relentrycachectx = NULL;
 
 static List *LoadPublications(List *pubnames);
 static void publication_invalidation_cb(Datum arg, int cacheid,
@@ -345,6 +346,13 @@ pgoutput_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
 		/* Disable the streaming during the slot initialization mode. */
 		ctx->streaming = false;
 	}
+
+	if (relentrycachectx == NULL)
+		relentrycachectx = AllocSetContextCreate(CacheMemoryContext,
+												 "logical replication relation entry cache context",
+												 ALLOCSET_SMALL_SIZES);
+	else
+		MemoryContextReset(relentrycachectx);
 }
 
 /*
@@ -464,7 +472,7 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 		MemoryContext oldctx;
 
 		/* Map must live as long as the session does. */
-		oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+		oldctx = MemoryContextSwitchTo(relentrycachectx);
 
 		/*
 		 * Make copies of the TupleDescs that will live as long as the map
-- 
2.43.0

---

From: vignesh C <[email-masked]>
Date: Thu, 26 Dec 2024 17:30:37 +0530

> in the attached patch. Thoughts?

Here is an updated version which includes registers to reset the
memory context that is in-line with a recently committed patch at [1].
[1] - https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=cfd6cbcf9be078fbdf9587014231a5772039b386

Regards,
Vignesh
From abf964263e5de78d44f6235f90789cfe5143b534 Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Thu, 26 Dec 2024 11:37:43 +0530
Subject: [PATCH v2] Fix memory leak in pgoutput relation attribute map

The pgoutput module caches relation attribute map only when validating the
RelationSyncEntry.  However, this was not getting freed incase of SQL functions
like pg_logical_slot_{get,peek}_changes() which would bloat its cache memory usage.
Every pg_logical_slot_{get,peek}_changes() call for changes on partition table
creates more bloat. To address this, this relation attribute map is allocated in
the plugin's cachectx which will be freed while the context is freed at
the end of pg_logical_slot_{get,peek}_changes() execution.
---
 src/backend/replication/pgoutput/pgoutput.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 6db780d733..6c19cc1bb2 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1197,8 +1197,8 @@ init_tuple_slot(PGOutputData *data, Relation relation,
 		TupleDesc	indesc = RelationGetDescr(relation);
 		TupleDesc	outdesc = RelationGetDescr(ancestor);
 
-		/* Map must live as long as the session does. */
-		oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+		/* Map must live as long as the logical decoding context. */
+		oldctx = MemoryContextSwitchTo(data->cachectx);

-- 
2.43.0

From 8ebf0403ea4f972b8cc3e58fc153e38f0e1abc38 Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Thu, 26 Dec 2024 12:05:33 +0530
Subject: [PATCH v2_PG15] Fix memory leak in pgoutput relation attribute map

The pgoutput module caches relation attribute map only when validating the
RelationSyncEntry.  However, this was not getting freed incase of SQL functions
like pg_logical_slot_{get,peek}_changes() which would bloat its cache memory usage.
Every pg_logical_slot_{get,peek}_changes() call for changes on partition table
creates more bloat. To address this, this relation attribute map is allocated in
the plugin's cachectx which will be freed while the context is freed at
the end of pg_logical_slot_{get,peek}_changes() execution.
---
 src/backend/replication/pgoutput/pgoutput.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index e178bd77ab..64f62de635 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1139,8 +1139,8 @@ init_tuple_slot(PGOutputData *data, Relation relation,
 		TupleDesc	indesc = RelationGetDescr(relation);
 		TupleDesc	outdesc = RelationGetDescr(ancestor);
 
-		/* Map must live as long as the session does. */
-		oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+		/* Map must live as long as the logical decoding context. */
+		oldctx = MemoryContextSwitchTo(data->cachectx);
 
 		entry->attrmap = build_attrmap_by_name_if_req(indesc, outdesc);
 
-- 
2.43.0

From f1e2c053c50248595dc6539a606665336449e0e0 Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Thu, 26 Dec 2024 15:11:09 +0530
Subject: [PATCH v2_PG14-] Fix memory leak in pgoutput relation attribute map

The pgoutput module caches relation attribute map and frees it upon
invalidation.  However, this was not getting freed incase of SQL functions like
pg_logical_slot_{get,peek}_changes() which would bloat its cache memory usage.
Every pg_logical_slot_{get,peek}_changes() call for changes on partition table
creates more bloat. To address this, this commit adds a new memory context
within the logical decoding context. This ensures that the lifespan of the
relation entry attribute map aligns with that of the logical decoding context.
The context is tracked with a static variable whose state is reset with a
MemoryContext reset callback attached to PGOutputData->context, so as ABI
compatibility is preserved in stable branches.
---
 src/backend/replication/pgoutput/pgoutput.c | 33 +++++++++++++++++++--
 1 file changed, 30 insertions(+), 3 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 9fd879ee0c..577db5909d 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -72,6 +72,13 @@ static bool in_streaming;
  */
 static MemoryContext pubctx = NULL;
 
+/*
+ * Private memory context for relation attribute map, created in
+ * PGOutputData->context when starting pgoutput, and set to NULL when its
+ * parent context is reset via a dedicated MemoryContextCallback.
+ */
+static MemoryContext cachectx = NULL;
+
 static List *LoadPublications(List *pubnames);
 static void publication_invalidation_cb(Datum arg, int cacheid,
 										uint32 hashvalue);
@@ -268,6 +275,15 @@ pgoutput_pubctx_reset_callback(void *arg)
 	pubctx = NULL;
 }
 
+/*
+ * Callback of PGOutputData->context in charge of cleaning cachectx.
+ */
+static void
+pgoutput_cachectx_reset_callback(void *arg)
+{
+	cachectx = NULL;
+}
+
 /*
  * Initialize this plugin
  */
@@ -278,6 +294,7 @@ pgoutput_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
 	PGOutputData *data = palloc0(sizeof(PGOutputData));
 	static bool publication_callback_registered = false;
 	MemoryContextCallback *mcallback;
+	MemoryContextCallback *cachectx_mcallback;
 
 	/* Create our memory context for private allocations. */
 	data->context = AllocSetContextCreate(ctx->context,
@@ -293,6 +310,15 @@ pgoutput_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
 	mcallback->func = pgoutput_pubctx_reset_callback;
 	MemoryContextRegisterResetCallback(ctx->context, mcallback);
 
+	Assert(cachectx == NULL);
+	cachectx = AllocSetContextCreate(ctx->context,
+									 "logical replication cache context",
+									 ALLOCSET_SMALL_SIZES);
+
+	cachectx_mcallback = palloc0(sizeof(MemoryContextCallback));
+	cachectx_mcallback->func = pgoutput_cachectx_reset_callback;
+	MemoryContextRegisterResetCallback(ctx->context, cachectx_mcallback);
+
 	ctx->output_plugin_private = data;
 
 	/* This plugin uses binary protocol. */
@@ -490,7 +516,7 @@ maybe_send_schema(LogicalDecodingContext *ctx,
 		MemoryContext oldctx;
 
 		/* Map must live as long as the session does. */
-		oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+		oldctx = MemoryContextSwitchTo(cachectx);
 
 		/*
 		 * Make copies of the TupleDescs that will live as long as the map
@@ -812,8 +838,8 @@ pgoutput_origin_filter(LogicalDecodingContext *ctx,
 /*
  * Shutdown the output plugin.
  *
- * Note, we don't need to clean the data->context and pubctx as they are
- * child contexts of the ctx->context so they will be cleaned up by logical
+ * Note, we don't need to clean the data->context, pubctx, and cachectx as they
+ * are child contexts of the ctx->context so they will be cleaned up by logical
  * decoding machinery.
  */
 static void
@@ -827,6 +853,7 @@ pgoutput_shutdown(LogicalDecodingContext *ctx)
 
 	/* Better safe than sorry */
 	pubctx = NULL;
+	cachectx = NULL;
 }
 
 /*
-- 
2.43.0

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Fri, 27 Dec 2024 08:13:53 +0000

On Thursday, December 26, 2024 8:01 PM vignesh C <[email-masked]> wrote:
> 
> Here is an updated version which includes registers to reset the memory
> context that is in-line with a recently committed patch at [1].

Thanks for updating patches ! They look good to me.

Just to confirm, would the other stuff (streamed_txns) that allocated under
CacheMemoryContext also leaks memory ? I think it's OK to change them
separately if it does but just would like to confirm if there is a risk.

> [1] -
> https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=cfd6cbc
> f9be078fbdf9587014231a5772039b386

Best Regards,
Hou zj

---

From: Michael Paquier <[email-masked]>
Date: Mon, 30 Dec 2024 13:42:31 +0900

> Thanks for updating patches ! They look good to me.

Fine by me as well.  I had a bit of time today, so I've taken care of
all this one down to 15 for now after checking each branch.

In the version of the patch for 14 and 13, why don't you just use a
single reset callback to handle both of cachectx and pubctx at the
same time?  That would be simpler.

+/*

This comment block is a copy-paste of the previous one, let's just
stick both declarations together.

> separately if it does but just would like to confirm if there is a risk.

Yeah, let's tackle this stuff separately and remove more the
dependency to CacheMemoryContext.
--
Michael