From 647334b5cb15926db460560c2e1cedbf33715a73 Mon Sep 17 00:00:00 2001
From: Kyotaro Horiguchi <horiguchi.kyotaro@lab.ntt.co.jp>
Date: Tue, 16 Oct 2018 13:04:30 +0900
Subject: [PATCH 1/3] Remove entries that haven't been used for a certain time

Catcache entries can be left alone for several reasons. It is not
desirable that they eat up memory. With this patch, This adds
consideration of removal of entries that haven't been used for a
certain time before enlarging the hash array.
---
 doc/src/sgml/config.sgml                      |  38 ++++++
 src/backend/access/transam/xact.c             |   5 +
 src/backend/utils/cache/catcache.c            | 166 ++++++++++++++++++++++++--
 src/backend/utils/misc/guc.c                  |  23 ++++
 src/backend/utils/misc/postgresql.conf.sample |   2 +
 src/include/utils/catcache.h                  |  28 ++++-
 6 files changed, 254 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index db1a2d4e74..4f4654120e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1662,6 +1662,44 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target">
+      <term><varname>syscache_memory_target</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_memory_target</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory to which syscache is expanded
+        without pruning. The value defaults to 0, indicating that pruning is
+        always considered. After exceeding this size, syscache pruning is
+        considered according to
+        <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep
+        certain amount of syscache entries with intermittent usage, try
+        increase this setting.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age">
+      <term><varname>syscache_prune_min_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the minimum amount of unused time in seconds at which a
+        syscache entry is considered to be removed. -1 indicates that syscache
+        pruning is disabled at all. The value defaults to 600 seconds
+        (<literal>10 minutes</literal>). The syscache entries that are not
+        used for the duration can be removed to prevent syscache bloat. This
+        behavior is suppressed until the size of syscache exceeds
+        <xref linkend="guc-syscache-memory-target"/>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index d967400384..71ae0daf17 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -733,7 +733,12 @@ void
 SetCurrentStatementStartTimestamp(void)
 {
 	if (!IsParallelWorker())
+	{
 		stmtStartTimestamp = GetCurrentTimestamp();
+
+		/* Set this timestamp as aproximated current time */
+		SetCatCacheClock(stmtStartTimestamp);
+	}
 	else
 		Assert(stmtStartTimestamp != 0);
 }
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index b31fd5acea..09d5a9a520 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -71,9 +71,24 @@
 #define CACHE6_elog(a,b,c,d,e,f,g)
 #endif
 
+/*
+ * GUC variable to define the minimum size of hash to cosider entry eviction.
+ * This variable is shared among various cache mechanisms.
+ */
+int cache_memory_target = 0;
+
+/* GUC variable to define the minimum age of entries that will be cosidered to
+ * be evicted in seconds. This variable is shared among various cache
+ * mechanisms.
+ */
+int cache_prune_min_age = 600;
+
 /* Cache management header --- pointer is NULL until created */
 static CatCacheHeader *CacheHdr = NULL;
 
+/* Timestamp used for any operation on caches. */
+TimestampTz	catcacheclock = 0;
+
 static inline HeapTuple SearchCatCacheInternal(CatCache *cache,
 					   int nkeys,
 					   Datum v1, Datum v2,
@@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct)
 		CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys,
 						 cache->cc_keyno, ct->keys);
 
+	cache->cc_tupsize -= ct->size;
 	pfree(ct);
 
 	--cache->cc_ntup;
@@ -841,6 +857,7 @@ InitCatCache(int id,
 	cp->cc_nkeys = nkeys;
 	for (i = 0; i < nkeys; ++i)
 		cp->cc_keyno[i] = key[i];
+	cp->cc_tupsize = 0;
 
 	/*
 	 * new cache is initialized as far as we can go for now. print some
@@ -858,9 +875,129 @@ InitCatCache(int id,
 	 */
 	MemoryContextSwitchTo(oldcxt);
 
+	/* initilize catcache reference clock if haven't done yet */
+	if (catcacheclock == 0)
+		catcacheclock = GetCurrentTimestamp();
+
 	return cp;
 }
 
+/*
+ * CatCacheCleanupOldEntries - Remove infrequently-used entries
+ *
+ * Catcache entries can be left alone for several reasons. We remove them if
+ * they are not accessed for a certain time to prevent catcache from
+ * bloating. The eviction is performed with the similar algorithm with buffer
+ * eviction using access counter. Entries that are accessed several times can
+ * live longer than those that have had no access in the same duration.
+ */
+static bool
+CatCacheCleanupOldEntries(CatCache *cp)
+{
+	int			i;
+	int			nremoved = 0;
+	size_t		hash_size;
+#ifdef CATCACHE_STATS
+	/* These variables are only for debugging purpose */
+	int			ntotal = 0;
+	/*
+	 * nth element in nentries stores the number of cache entries that have
+	 * lived unaccessed for corresponding multiple in ageclass of
+	 * cache_prune_min_age. The index of nremoved_entry is the value of the
+	 * clock-sweep counter, which takes from 0 up to 2.
+	 */
+	double		ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0};
+	int			nentries[] = {0, 0, 0, 0, 0, 0};
+	int			nremoved_entry[3] = {0, 0, 0};
+	int			j;
+#endif
+
+	/* Return immediately if no pruning is wanted */
+	if (cache_prune_min_age < 0)
+		return false;
+
+	/*
+	 * Return without pruning if the size of the hash is below the target.
+	 */
+	hash_size = cp->cc_nbuckets * sizeof(dlist_head);
+	if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L)
+		return false;
+	
+	/* Search the whole hash for entries to remove */
+	for (i = 0; i < cp->cc_nbuckets; i++)
+	{
+		dlist_mutable_iter iter;
+
+		dlist_foreach_modify(iter, &cp->cc_bucket[i])
+		{
+			CatCTup    *ct = dlist_container(CatCTup, cache_elem, iter.cur);
+			long entry_age;
+			int us;
+
+
+			/*
+			 * Calculate the duration from the time of the last access to the
+			 * "current" time. Since catcacheclock is not advanced within a
+			 * transaction, the entries that are accessed within the current
+			 * transaction won't be pruned.
+			 */
+			TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us);
+
+#ifdef CATCACHE_STATS
+			/* count catcache entries for each age class */
+			ntotal++;
+			for (j = 0 ;
+				 ageclass[j] != 0.0 &&
+					 entry_age > cache_prune_min_age * ageclass[j] ;
+				 j++);
+			if (ageclass[j] == 0.0) j--;
+			nentries[j]++;
+#endif
+
+			/*
+			 * Try to remove entries older than cache_prune_min_age seconds.
+			 * Entries that are not accessed after last pruning are removed in
+			 * that seconds, and that has been accessed several times are
+			 * removed after leaving alone for up to three times of the
+			 * duration. We don't try shrink buckets since pruning effectively
+			 * caps catcache expansion in the long term.
+			 */
+			if (entry_age > cache_prune_min_age)
+			{
+#ifdef CATCACHE_STATS
+				Assert (ct->naccess >= 0 && ct->naccess <= 2);
+				nremoved_entry[ct->naccess]++;
+#endif
+				if (ct->naccess > 0)
+					ct->naccess--;
+				else
+				{
+					if (!ct->c_list || ct->c_list->refcount == 0)
+					{
+						CatCacheRemoveCTup(cp, ct);
+						nremoved++;
+					}
+				}
+			}
+		}
+	}
+
+#ifdef CATCACHE_STATS
+	ereport(DEBUG1,
+			(errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)",
+					 nremoved, ntotal,
+					 ageclass[0] * cache_prune_min_age, nentries[0],
+					 ageclass[1] * cache_prune_min_age, nentries[1],
+					 ageclass[2] * cache_prune_min_age, nentries[2],
+					 ageclass[3] * cache_prune_min_age, nentries[3],
+					 ageclass[4] * cache_prune_min_age, nentries[4],
+					 nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]),
+			 errhidestmt(true)));
+#endif
+
+	return nremoved > 0;
+}
+
 /*
  * Enlarge a catcache, doubling the number of buckets.
  */
@@ -1274,6 +1411,11 @@ SearchCatCacheInternal(CatCache *cache,
 		 */
 		dlist_move_head(bucket, &ct->cache_elem);
 
+		/* Update access information for pruning */
+		if (ct->naccess < 2)
+			ct->naccess++;
+		ct->lastaccess = catcacheclock;
+
 		/*
 		 * If it's a positive entry, bump its refcount and return it. If it's
 		 * negative, we can report failure to the caller.
@@ -1819,11 +1961,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	CatCTup    *ct;
 	HeapTuple	dtp;
 	MemoryContext oldcxt;
+	int			tupsize = 0;
 
 	/* negative entries have no tuple associated */
 	if (ntp)
 	{
 		int			i;
+		int			tupsize;
 
 		Assert(!negative);
 
@@ -1842,13 +1986,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 		/* Allocate memory for CatCTup and the cached tuple in one go */
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
 
-		ct = (CatCTup *) palloc(sizeof(CatCTup) +
-								MAXIMUM_ALIGNOF + dtp->t_len);
+		tupsize = sizeof(CatCTup) +	MAXIMUM_ALIGNOF + dtp->t_len;
+		ct = (CatCTup *) palloc(tupsize);
 		ct->tuple.t_len = dtp->t_len;
 		ct->tuple.t_self = dtp->t_self;
 		ct->tuple.t_tableOid = dtp->t_tableOid;
 		ct->tuple.t_data = (HeapTupleHeader)
 			MAXALIGN(((char *) ct) + sizeof(CatCTup));
+		ct->size = tupsize;
 		/* copy tuple contents */
 		memcpy((char *) ct->tuple.t_data,
 			   (const char *) dtp->t_data,
@@ -1876,8 +2021,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	{
 		Assert(negative);
 		oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
-		ct = (CatCTup *) palloc(sizeof(CatCTup));
-
+		tupsize = sizeof(CatCTup);
+		ct = (CatCTup *) palloc(tupsize);
 		/*
 		 * Store keys - they'll point into separately allocated memory if not
 		 * by-value.
@@ -1898,17 +2043,24 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments,
 	ct->dead = false;
 	ct->negative = negative;
 	ct->hash_value = hashValue;
+	ct->naccess = 0;
+	ct->lastaccess = catcacheclock;
+	ct->size = tupsize;
 
 	dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem);
 
 	cache->cc_ntup++;
 	CacheHdr->ch_ntup++;
+	cache->cc_tupsize += tupsize;
 
 	/*
-	 * If the hash table has become too full, enlarge the buckets array. Quite
-	 * arbitrarily, we enlarge when fill factor > 2.
+	 * If the hash table has become too full, try cleanup by removing
+	 * infrequently used entries to make a room for the new entry. If it
+	 * failed, enlarge the bucket array instead.  Quite arbitrarily, we try
+	 * this when fill factor > 2.
 	 */
-	if (cache->cc_ntup > cache->cc_nbuckets * 2)
+	if (cache->cc_ntup > cache->cc_nbuckets * 2 &&
+		!CatCacheCleanupOldEntries(cache))
 		RehashCatCache(cache);
 
 	return ct;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 6497393c03..28af4c8795 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -80,6 +80,7 @@
 #include "tsearch/ts_cache.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/catcache.h"
 #include "utils/guc_tables.h"
 #include "utils/float.h"
 #include "utils/memutils.h"
@@ -2167,6 +2168,28 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cache_memory_target", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum syscache size to keep."),
+			gettext_noop("Cache is not pruned before exceeding this size."),
+			GUC_UNIT_KB
+		},
+		&cache_memory_target,
+		0, 0, MAX_KILOBYTES,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the minimum unused duration of cache entries before removal."),
+			gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."),
+			GUC_UNIT_S
+		},
+		&cache_prune_min_age,
+		600, -1, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee9ec6a120..6bc1fc3e61 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -128,6 +128,8 @@
 #work_mem = 4MB				# min 64kB
 #maintenance_work_mem = 64MB		# min 1MB
 #autovacuum_work_mem = -1		# min 1MB, or -1 to use maintenance_work_mem
+#cache_memory_target = 0kB	# in kB
+#cache_prune_min_age = 600s	# -1 disables pruning
 #max_stack_depth = 2MB			# min 100kB
 #dynamic_shared_memory_type = posix	# the default is the first option
 					# supported by the operating system:
diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h
index 7b22f9c7bc..ace4178619 100644
--- a/src/include/utils/catcache.h
+++ b/src/include/utils/catcache.h
@@ -22,6 +22,7 @@
 
 #include "access/htup.h"
 #include "access/skey.h"
+#include "datatype/timestamp.h"
 #include "lib/ilist.h"
 #include "utils/relcache.h"
 
@@ -61,6 +62,7 @@ typedef struct catcache
 	slist_node	cc_next;		/* list link */
 	ScanKeyData cc_skey[CATCACHE_MAXKEYS];	/* precomputed key info for heap
 											 * scans */
+	int			cc_tupsize;		/* total amount of catcache tuples */
 
 	/*
 	 * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS
@@ -119,7 +121,9 @@ typedef struct catctup
 	bool		dead;			/* dead but not yet removed? */
 	bool		negative;		/* negative cache entry? */
 	HeapTupleData tuple;		/* tuple management header */
-
+	int			naccess;		/* # of access to this entry, up to 2  */
+	TimestampTz	lastaccess;		/* approx. timestamp of the last usage */
+	int			size;			/* palloc'ed size off this tuple */
 	/*
 	 * The tuple may also be a member of at most one CatCList.  (If a single
 	 * catcache is list-searched with varying numbers of keys, we may have to
@@ -189,6 +193,28 @@ typedef struct catcacheheader
 /* this extern duplicates utils/memutils.h... */
 extern PGDLLIMPORT MemoryContext CacheMemoryContext;
 
+/* for guc.c, not PGDLLPMPORT'ed */
+extern int cache_prune_min_age;
+extern int cache_memory_target;
+
+/* to use as access timestamp of catcache entries */
+extern TimestampTz catcacheclock;
+
+/*
+ * SetCatCacheClock - set timestamp for catcache access record
+ */
+static inline void
+SetCatCacheClock(TimestampTz ts)
+{
+	catcacheclock = ts;
+}
+
+static inline TimestampTz
+GetCatCacheClock(void)
+{
+	return catcacheclock;
+}
+
 extern void CreateCacheMemoryContext(void);
 
 extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid,
-- 
2.16.3

