truncating pg_multixact/members

Started by Alvaro Herreraabout 12 years ago31 messages
#1Alvaro Herrera
alvherre@2ndquadrant.com

I started looking at bug #8673 some days ago, and I identified three
separate issues that need fixing:

1. slru.c doesn't consider file names longer than 4 hexadecimal chars.

2. pg_multixact/members truncation requires more intelligence to avoid
removing files that are still needed. Right now we use modulo-2^32
arithmetic, but this doesn't work because the useful range can span
longer than what we can keep within that range.

3. New pg_multixact/members generation requires more intelligence to
avoid stomping on files from the previous wraparound cycle. Right now
there is no defense against this at all.

Fixing (1) is simple: we can have each SLRU user declare how many digits
to have in file names. All existing users but pg_multixact/members
should declare 4 digits; that one should declare 5. That way, the
correct number of zeroes are allocated at the start point and we get
nice, equal-width file names. Eventually, predicate.c can change to
wider file names and get rid of some strange code it has to deal with
overrun.

For 9.3, I propose we skip this and tweak the code to consider files
whose names are 4 or 5 chars in length, to remain compatible with
existing installations that have pg_multixact/member having a mixture of
4-char and 5-char file names.

For (2) a simple-minded proposal is to have a new SlruScanDirectory
callback that knows to delete only files within a certain range. Then,
at truncate time, collect the existing valid range (i.e. files
containing multixacts between oldestMulti and nextMulti) and delete
files outside this range. However there is a race condition: if
pg_multixact/member grows concurrently while the truncation is
happening, the new files would be outside the range and would be
deleted. There is no bound to how much the directory can grow, so it
doesn't seem reasonable to just add some arbitrary safety limit. I see
three possible fixes:

#2a Interlock directory truncation with new file generation: in
GetNewMultiXactId and TruncateMultiXact grab a lock (perhaps a boolean
in MultiXactState, or perhaps just a new LWLock) to exclude each from
the other. That way, truncation can obtain a range that will continue
to be meaningful until truncation is complete, and no new files will be
erased.

#2b During truncation, first obtain a directory listing, *then* compute
the range of files to keep, then delete files outside that range but
only if they are present in the listing previously obtained. That way,
files created during truncation are not removed.

#2c At start of truncation, save end-of-range in MultiXactState. This
state is updated by GetNewMultiXactId as new files are created. That
way, before each new file is created, the truncation routine knows to
skip it.

I don't like #2a because of concurrency loss, and I don't like #2b
because it seems ugly and potentially slow. #2c seems to most
reasonable way to attack this problem, but if somebody has a differing
opinion please voice it.

For (3) there is a hand-wavy idea that we can compare the oldest offset
to the next offset, and avoid enlarging (raise an ERROR) if an overrun
occurs; but we don't have the oldest offset stored anywhere convenient.
We would have to scan multixact/offsets at start of service to determine
it, and then perhaps keep it in MultiXactState. Arguably this info
should be part of pg_control, but we don't have that in 9.3 so we'll
have to find some other idea.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#2Kevin Grittner
kgrittn@ymail.com
In reply to: Alvaro Herrera (#1)
Re: truncating pg_multixact/members

Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

1. slru.c doesn't consider file names longer than 4 hexadecimal chars.

Fixing (1) is simple: we can have each SLRU user declare how many digits
to have in file names.  All existing users but pg_multixact/members
should declare 4 digits; that one should declare 5.  That way, the
correct number of zeroes are allocated at the start point and we get
nice, equal-width file names.  Eventually, predicate.c can change to
wider file names and get rid of some strange code it has to deal with
overrun.

That would be nice.

There would be the issue of how to deal with pg_upgrade, though. If
I remember correctly, there is no strong reason not to blow away
any existing files in the pg_serial subdirectory at startup (the
way NOTIFY code does), and at one point I had code to do that.  I
think we took that code out because the files would be deleted
"soon enough" anyway.  Barring objection, deleting them at startup
seems like a sane way to handle pg_upgrade issues when we do
increase the filename size.

--
Kevin Grittner
EDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Kevin Grittner (#2)
Re: truncating pg_multixact/members

Kevin Grittner wrote:

Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

1. slru.c doesn't consider file names longer than 4 hexadecimal chars.

Fixing (1) is simple: we can have each SLRU user declare how many digits
to have in file names.� All existing users but pg_multixact/members
should declare 4 digits; that one should declare 5.� That way, the
correct number of zeroes are allocated at the start point and we get
nice, equal-width file names.� Eventually, predicate.c can change to
wider file names and get rid of some strange code it has to deal with
overrun.

That would be nice.

There would be the issue of how to deal with pg_upgrade, though. If
I remember correctly, there is no strong reason not to blow away
any existing files in the pg_serial subdirectory at startup (the
way NOTIFY code does), and at one point I had code to do that.� I
think we took that code out because the files would be deleted
"soon enough" anyway.� Barring objection, deleting them at startup
seems like a sane way to handle pg_upgrade issues when we do
increase the filename size.

Agreed. It's easy to have the files deleted at startup now that the
truncation stuff uses a callback. There is already a callback that's
used to delete all files, so you won't need to write any code to make it
behave that way.

FWIW for pg_multixact/members during pg_upgrade from 9.3 to 9.4 we will
need to rename existing files, prepending a zero to each file whose name
is four chars in length.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#1)
3 attachment(s)
Re: truncating pg_multixact/members

Alvaro Herrera wrote:

1. slru.c doesn't consider file names longer than 4 hexadecimal chars.

For 9.3, I propose we skip this and tweak the code to consider files
whose names are 4 or 5 chars in length, to remain compatible with
existing installations that have pg_multixact/member having a mixture of
4-char and 5-char file names.

Attached is a patch for this.

2. pg_multixact/members truncation requires more intelligence to avoid
removing files that are still needed. Right now we use modulo-2^32
arithmetic, but this doesn't work because the useful range can span
longer than what we can keep within that range.

#2c At start of truncation, save end-of-range in MultiXactState. This
state is updated by GetNewMultiXactId as new files are created. That
way, before each new file is created, the truncation routine knows to
skip it.

Attached is a patch implementing this.

I also attach a patch implementing a "burn multixact" utility, initially
coded by Andres Freund, tweaked by me. I used it to run a bunch of
wraparound cycles and everything seems to behave as expected. (I don't
recommend applying this patch; I'm posting merely because it's a very
useful debugging tool.)

One problem I see is length of time before freezing multis: they live
for far too long, causing the SLRU files to eat way too much disk space.
I ran burnmulti in a loop, creating multis of 3 members each, with a min
freeze age of 50 million, and this leads to ~770 files in
pg_multixact/offsets and ~2900 files in pg_multixact/members. Each file
is 32 pages long. 256kB apiece. Probably enough to be bothersome.

I think for computing the freezing point for multis, we should slash
min_freeze_age by 10 or something like that. Or just set a hardcoded
one million.

3. New pg_multixact/members generation requires more intelligence to
avoid stomping on files from the previous wraparound cycle. Right now
there is no defense against this at all.

I still have no idea how to attack this.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

0001-pg_burn_multixact-utility.patchtext/x-diff; charset=us-asciiDownload
>From b264bfe61b315a5f65d72b9550592cc9f73bf0a8 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 31 Dec 2013 00:42:11 -0300
Subject: [PATCH 1/3] pg_burn_multixact utility

Andres Freund, minor tweaks by me
---
 contrib/pageinspect/heapfuncs.c          |   42 ++++++++++++++++++++++++++++++
 contrib/pageinspect/pageinspect--1.1.sql |    5 ++++
 src/backend/access/heap/heapam.c         |    2 +-
 src/backend/access/transam/multixact.c   |   12 +++++----
 src/include/access/multixact.h           |    3 ++-
 5 files changed, 57 insertions(+), 7 deletions(-)

diff --git a/contrib/pageinspect/heapfuncs.c b/contrib/pageinspect/heapfuncs.c
index 6d8f6f1..93a3317 100644
--- a/contrib/pageinspect/heapfuncs.c
+++ b/contrib/pageinspect/heapfuncs.c
@@ -29,6 +29,8 @@
 #include "funcapi.h"
 #include "utils/builtins.h"
 #include "miscadmin.h"
+#include "access/multixact.h"
+#include "access/transam.h"
 
 Datum		heap_page_items(PG_FUNCTION_ARGS);
 
@@ -224,3 +226,43 @@ heap_page_items(PG_FUNCTION_ARGS)
 	else
 		SRF_RETURN_DONE(fctx);
 }
+
+extern Datum
+pg_burn_multixact(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pg_burn_multixact);
+
+Datum
+pg_burn_multixact(PG_FUNCTION_ARGS)
+{
+	int		rep = PG_GETARG_INT32(0);
+	int		size = PG_GETARG_INT32(1);
+	MultiXactMember *members;
+	MultiXactId ret;
+	TransactionId id = ReadNewTransactionId() - size;
+	int		i;
+
+	if (rep < 1)
+		elog(ERROR, "need to burn, burn, burn");
+
+	members = palloc(size * sizeof(MultiXactMember));
+	for (i = 0; i < size; i++)
+	{
+		members[i].xid = id++;
+		members[i].status = MultiXactStatusForShare;
+
+		if (!TransactionIdIsNormal(members[i].xid))
+		{
+			id = FirstNormalTransactionId;
+			members[i].xid = id++;
+		}
+	}
+
+	MultiXactIdSetOldestMember();
+
+	for (i = 0; i < rep; i++)
+	{
+		ret = MultiXactIdCreateFromMembers(size, members, true);
+	}
+
+	PG_RETURN_INT64((int64) ret);
+}
diff --git a/contrib/pageinspect/pageinspect--1.1.sql b/contrib/pageinspect/pageinspect--1.1.sql
index 22a47d5..b895246 100644
--- a/contrib/pageinspect/pageinspect--1.1.sql
+++ b/contrib/pageinspect/pageinspect--1.1.sql
@@ -105,3 +105,8 @@ CREATE FUNCTION fsm_page_contents(IN page bytea)
 RETURNS text
 AS 'MODULE_PATHNAME', 'fsm_page_contents'
 LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_burn_multixact(num int4, size int4)
+RETURNS int4
+AS 'MODULE_PATHNAME', 'pg_burn_multixact'
+LANGUAGE C STRICT;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 90e9e6f..1b8469f 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -5544,7 +5544,7 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
 		 * Create a new multixact with the surviving members of the previous
 		 * one, to set as new Xmax in the tuple.
 		 */
-		xid = MultiXactIdCreateFromMembers(nnewmembers, newmembers);
+		xid = MultiXactIdCreateFromMembers(nnewmembers, newmembers, false);
 		*flags |= FRM_RETURN_IS_MULTI;
 	}
 
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 6ce0eb2..2f87a1e 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -349,7 +349,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
 	members[1].xid = xid2;
 	members[1].status = status2;
 
-	newMulti = MultiXactIdCreateFromMembers(2, members);
+	newMulti = MultiXactIdCreateFromMembers(2, members, false);
 
 	debug_elog3(DEBUG2, "Create: %s",
 				mxid_to_string(newMulti, 2, members));
@@ -415,7 +415,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
 		 */
 		member.xid = xid;
 		member.status = status;
-		newMulti = MultiXactIdCreateFromMembers(1, &member);
+		newMulti = MultiXactIdCreateFromMembers(1, &member, false);
 
 		debug_elog4(DEBUG2, "Expand: %u has no members, create singleton %u",
 					multi, newMulti);
@@ -467,7 +467,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
 
 	newMembers[j].xid = xid;
 	newMembers[j++].status = status;
-	newMulti = MultiXactIdCreateFromMembers(j, newMembers);
+	newMulti = MultiXactIdCreateFromMembers(j, newMembers, false);
 
 	pfree(members);
 	pfree(newMembers);
@@ -681,7 +681,7 @@ ReadNextMultiXactId(void)
  * NB: the passed members[] array will be sorted in-place.
  */
 MultiXactId
-MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members)
+MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members, bool nocache)
 {
 	MultiXactId multi;
 	MultiXactOffset offset;
@@ -701,7 +701,9 @@ MultiXactIdCreateFromMembers(int nmembers, MultiXactMember *members)
 	 * corner cases where someone else added us to a MultiXact without our
 	 * knowledge, but it's not worth checking for.)
 	 */
-	multi = mXactCacheGetBySet(nmembers, members);
+	multi = nocache ? InvalidMultiXactId :
+		mXactCacheGetBySet(nmembers, members);
+
 	if (MultiXactIdIsValid(multi))
 	{
 		debug_elog2(DEBUG2, "Create: in cache!");
diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h
index 6aceaeb..1180e1f 100644
--- a/src/include/access/multixact.h
+++ b/src/include/access/multixact.h
@@ -79,10 +79,11 @@ typedef struct xl_multixact_create
 extern MultiXactId MultiXactIdCreate(TransactionId xid1,
 				  MultiXactStatus status1, TransactionId xid2,
 				  MultiXactStatus status2);
+extern MultiXactId CreateMultiXactId(int nmembers, MultiXactMember *members, bool nocache);
 extern MultiXactId MultiXactIdExpand(MultiXactId multi, TransactionId xid,
 				  MultiXactStatus status);
 extern MultiXactId MultiXactIdCreateFromMembers(int nmembers,
-							 MultiXactMember *members);
+							 MultiXactMember *members, bool nocache);
 
 extern MultiXactId ReadNextMultiXactId(void);
 extern bool MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly);
-- 
1.7.10.4

0002-handle-wraparound-during-trunc-for-multixact-members.patchtext/x-diff; charset=us-asciiDownload
>From 44ac15e12ac2af4df613087098f9be573517e257 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 31 Dec 2013 00:43:38 -0300
Subject: [PATCH 2/3] handle wraparound during trunc for multixact/members

---
 src/backend/access/transam/multixact.c |  124 +++++++++++++++++++++++++++++---
 src/backend/access/transam/slru.c      |    5 ++
 2 files changed, 119 insertions(+), 10 deletions(-)

diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 2f87a1e..ba730d6 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -63,6 +63,8 @@
  */
 #include "postgres.h"
 
+#include <unistd.h>
+
 #include "access/multixact.h"
 #include "access/slru.h"
 #include "access/transam.h"
@@ -577,8 +579,13 @@ MultiXactIdSetOldestMember(void)
 		 * another someone else could compute an OldestVisibleMXactId that
 		 * would be after the value we are going to store when we get control
 		 * back.  Which would be wrong.
+		 *
+		 * Note that a shared lock is sufficient, because it's enough to stop
+		 * someone from advancing nextMXact; and nobody else could be trying to
+		 * write to our OldestMember entry, only reading (and we assume storing
+		 * it is atomic.)
 		 */
-		LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
+		LWLockAcquire(MultiXactGenLock, LW_SHARED);
 
 		/*
 		 * We have to beware of the possibility that nextMXact is in the
@@ -1546,7 +1553,7 @@ AtEOXact_MultiXact(void)
 
 /*
  * AtPrepare_MultiXact
- *		Save multixact state at 2PC tranasction prepare
+ *		Save multixact state at 2PC transaction prepare
  *
  * In this phase, we only store our OldestMemberMXactId value in the two-phase
  * state file.
@@ -2241,7 +2248,6 @@ ExtendMultiXactMember(MultiXactOffset offset, int nmembers)
 	{
 		int			flagsoff;
 		int			flagsbit;
-		int			difference;
 
 		/*
 		 * Only zero when at first entry of a page.
@@ -2262,10 +2268,25 @@ ExtendMultiXactMember(MultiXactOffset offset, int nmembers)
 			LWLockRelease(MultiXactMemberControlLock);
 		}
 
-		/* Advance to next page (OK if nmembers goes negative) */
-		difference = MULTIXACT_MEMBERS_PER_PAGE - offset % MULTIXACT_MEMBERS_PER_PAGE;
-		offset += difference;
-		nmembers -= difference;
+		/*
+		 * Advance to next page, taking care to properly handle the wraparound
+		 * case.
+		 */
+		if ((unsigned int) (offset + nmembers) < offset)
+		{
+			uint32		difference = offset + MULTIXACT_MEMBERS_PER_PAGE;
+
+			nmembers -= (unsigned int) (MULTIXACT_MEMBERS_PER_PAGE - difference);
+			offset = 0;
+		}
+		else
+		{
+			int			difference;
+
+			difference = MULTIXACT_MEMBERS_PER_PAGE - offset % MULTIXACT_MEMBERS_PER_PAGE;
+			nmembers -= difference;
+			offset += difference;
+		}
 	}
 }
 
@@ -2322,6 +2343,72 @@ GetOldestMultiXactId(void)
 	return oldestMXact;
 }
 
+/*
+ * SlruScanDirectory callback.
+ * 		This callback deletes segments that are outside the range determined by
+ * 		the given page numbers.
+ *
+ * Both range endpoints are exclusive (that is, segments containing any of
+ * those pages are kept.)
+ */
+typedef struct SlruScanDirPageRange
+{
+	int		rangeStart;
+	int		rangeEnd;
+} SlruScanDirPageRange;
+
+static bool
+SlruScanDirCbRemoveMembers(SlruCtl ctl, char *filename, int segpage,
+						   void *data)
+{
+	SlruScanDirPageRange *range = (SlruScanDirPageRange *) data;
+	MultiXactOffset	nextOffset;
+
+	if (range->rangeStart == range->rangeEnd)
+		return false;		/* easy case out */
+
+	/*
+	 * To ensure that no segment is spuriously removed, we must keep track
+	 * of new segments added since the start of the directory scan; to do this,
+	 * we update our end-of-range point as we run.
+	 *
+	 * As an optimization, we can skip looking at shared memory if we know for
+	 * certain that the current segment must be kept.  This is so because
+	 * nextOffset never decreases, and we never increase rangeStart during any
+	 * one run.
+	 */
+	if (!((range->rangeStart > range->rangeEnd &&
+		   segpage > range->rangeEnd && segpage < range->rangeStart) ||
+		  (range->rangeStart < range->rangeEnd &&
+		   (segpage < range->rangeStart || segpage > range->rangeEnd))))
+		return false;
+
+	/*
+	 * Update our idea of the end of the live range.
+	 */
+	LWLockAcquire(MultiXactGenLock, LW_SHARED);
+	nextOffset = MultiXactState->nextOffset;
+	LWLockRelease(MultiXactGenLock);
+	range->rangeEnd = MXOffsetToMemberPage(nextOffset);
+
+	/* Recheck the deletion condition.  If it still holds, perform it. */
+	if ((range->rangeStart > range->rangeEnd &&
+		 segpage > range->rangeEnd && segpage < range->rangeStart) ||
+		(range->rangeStart < range->rangeEnd &&
+		 (segpage < range->rangeStart || segpage > range->rangeEnd)))
+	{
+		char		path[MAXPGPATH];
+
+		snprintf(path, MAXPGPATH, "%s/%s", ctl->Dir, filename);
+		ereport(DEBUG2,
+				(errmsg("removing file \"%s\"", path)));
+		unlink(path);
+	}
+
+	return false;				/* keep going */
+}
+
+
 typedef struct mxtruncinfo
 {
 	int			earliestExistingPage;
@@ -2363,8 +2450,10 @@ void
 TruncateMultiXact(MultiXactId oldestMXact)
 {
 	MultiXactOffset oldestOffset;
+	MultiXactOffset	nextOffset;
 	mxtruncinfo trunc;
 	MultiXactId earliest;
+	SlruScanDirPageRange	pageRange;
 
 	/*
 	 * Note we can't just plow ahead with the truncation; it's possible that
@@ -2411,9 +2500,24 @@ TruncateMultiXact(MultiXactId oldestMXact)
 	SimpleLruTruncate(MultiXactOffsetCtl,
 					  MultiXactIdToOffsetPage(oldestMXact));
 
-	/* truncate MultiXactMembers and we're done */
-	SimpleLruTruncate(MultiXactMemberCtl,
-					  MXOffsetToMemberPage(oldestOffset));
+	/*
+	 * To truncate MultiXactMembers, we need to figure out the active page
+	 * range and delete all files outside that range.  The start point is the
+	 * start of the segment containing the oldest offset; an end point of the
+	 * segment containing the next offset to use is enough.  The end point is
+	 * updated as MultiXactMember gets extended concurrently, elsewhere.
+	 */
+	pageRange.rangeStart = MXOffsetToMemberPage(oldestOffset);
+	pageRange.rangeStart -= pageRange.rangeStart % SLRU_PAGES_PER_SEGMENT;
+
+	LWLockAcquire(MultiXactGenLock, LW_SHARED);
+	nextOffset = MultiXactState->nextOffset;
+	LWLockRelease(MultiXactGenLock);
+
+	pageRange.rangeEnd = MXOffsetToMemberPage(nextOffset);
+
+	SlruScanDirectory(MultiXactMemberCtl, SlruScanDirCbRemoveMembers,
+					  &pageRange);
 }
 
 /*
diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c
index 5e53593..a8480b0 100644
--- a/src/backend/access/transam/slru.c
+++ b/src/backend/access/transam/slru.c
@@ -1272,6 +1272,11 @@ SlruScanDirCbDeleteAll(SlruCtl ctl, char *filename, int segpage, void *data)
  * If the callback returns true, the scan is stopped.  The last return value
  * from the callback is returned.
  *
+ * The callback receives the following arguments: 1. the SlruCtl struct for the
+ * slru being truncated; 2. the filename being considered; 3. the page number
+ * for the first page of that file; 4. a pointer to the opaque data given to us
+ * by the caller.
+ *
  * Note that the ordering in which the directory is scanned is not guaranteed.
  *
  * Note that no locking is applied.
-- 
1.7.10.4

0003-Have-5-char-filenames-in-SlruScanDirectory.patchtext/x-diff; charset=us-asciiDownload
>From 2df77b55a900f576c4b2e97c9d1d2ed819b03f1c Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 31 Dec 2013 00:44:54 -0300
Subject: [PATCH 3/3] Have 5-char filenames in SlruScanDirectory

---
 src/backend/access/transam/slru.c |    8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c
index a8480b0..317b6a5 100644
--- a/src/backend/access/transam/slru.c
+++ b/src/backend/access/transam/slru.c
@@ -1293,8 +1293,12 @@ SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data)
 	cldir = AllocateDir(ctl->Dir);
 	while ((clde = ReadDir(cldir, ctl->Dir)) != NULL)
 	{
-		if (strlen(clde->d_name) == 4 &&
-			strspn(clde->d_name, "0123456789ABCDEF") == 4)
+		size_t	len;
+
+		len = strlen(clde->d_name);
+
+		if ((len == 4 || len == 5) &&
+			strspn(clde->d_name, "0123456789ABCDEF") == len)
 		{
 			segno = (int) strtol(clde->d_name, NULL, 16);
 			segpage = segno * SLRU_PAGES_PER_SEGMENT;
-- 
1.7.10.4

#5Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#4)
Re: truncating pg_multixact/members

On Mon, Dec 30, 2013 at 10:59 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

One problem I see is length of time before freezing multis: they live
for far too long, causing the SLRU files to eat way too much disk space.
I ran burnmulti in a loop, creating multis of 3 members each, with a min
freeze age of 50 million, and this leads to ~770 files in
pg_multixact/offsets and ~2900 files in pg_multixact/members. Each file
is 32 pages long. 256kB apiece. Probably enough to be bothersome.

I think for computing the freezing point for multis, we should slash
min_freeze_age by 10 or something like that. Or just set a hardcoded
one million.

Yeah. Since we expect mxids to be composed at a much lower rate than
xids, we can keep pg_multixact small without needing to increase the
rate of full table scans. However, it seems to me that we ought to
have GUCs for mxid_freeze_table_age and mxid_freeze_min_age. There's
no principled way to derive those values from the corresponding values
for XIDs, and I can't see any reason to suppose that we know how to
auto-tune brand new values better than we know how to auto-tune their
XID equivalents that we've had for years.

One million is probably a reasonable default for mxid_freeze_min_age, though.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#5)
Re: truncating pg_multixact/members

Robert Haas escribi�:

On Mon, Dec 30, 2013 at 10:59 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

One problem I see is length of time before freezing multis: they live
for far too long, causing the SLRU files to eat way too much disk space.
I ran burnmulti in a loop, creating multis of 3 members each, with a min
freeze age of 50 million, and this leads to ~770 files in
pg_multixact/offsets and ~2900 files in pg_multixact/members. Each file
is 32 pages long. 256kB apiece. Probably enough to be bothersome.

I think for computing the freezing point for multis, we should slash
min_freeze_age by 10 or something like that. Or just set a hardcoded
one million.

Yeah. Since we expect mxids to be composed at a much lower rate than
xids, we can keep pg_multixact small without needing to increase the
rate of full table scans. However, it seems to me that we ought to
have GUCs for mxid_freeze_table_age and mxid_freeze_min_age. There's
no principled way to derive those values from the corresponding values
for XIDs, and I can't see any reason to suppose that we know how to
auto-tune brand new values better than we know how to auto-tune their
XID equivalents that we've had for years.

One million is probably a reasonable default for mxid_freeze_min_age, though.

I didn't want to propose having new GUCs, but if there's no love for my
idea of deriving it from the Xid freeze policy, I guess it's the only
solution. Just keep in mind we will need to back-patch these new GUCs
to 9.3. Are there objections to this?

Also, what would be good names? Peter E. complained recently about the
word MultiXactId being exposed in some error messages; maybe "mxid" is
too short an abbreviation of that. Perhaps
multixactid_freeze_min_age = 1 million
multixactid_freeze_table_age = 3 million
?
I imagine this stuff would be described somewhere in the docs, perhaps
within the "routine maintenance" section somewhere.

FWIW the idea of having a glossary sounds good to me.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Andres Freund
andres@2ndquadrant.com
In reply to: Alvaro Herrera (#6)
Re: truncating pg_multixact/members

Hi,

On 2014-01-03 11:11:13 -0300, Alvaro Herrera wrote:

Yeah. Since we expect mxids to be composed at a much lower rate than
xids, we can keep pg_multixact small without needing to increase the
rate of full table scans.

I don't think that's necessarily true - there have been several
pg_controldata outputs posted lately which had more multis used than
xids. In workloads using explicit row locking or heavily used FKs that's
not that suprising.

However, it seems to me that we ought to
have GUCs for mxid_freeze_table_age and mxid_freeze_min_age. There's
no principled way to derive those values from the corresponding values
for XIDs, and I can't see any reason to suppose that we know how to
auto-tune brand new values better than we know how to auto-tune their
XID equivalents that we've had for years.

One million is probably a reasonable default for mxid_freeze_min_age, though.

I think setting mxid_freeze_min_age to something lower is fair game, I'd
even start at 100k or so. What I think is important is that we do *not*
set mxid_freeze_table_age to something very low. People justifiedly hate
anti-wraparound vacuums.

What's your thought about the autovacuum_freeze_max_age equivalent?

I am not sure about introducing new GUCs in the back branches, I don't
have a problem with it, but I am also not sure it's necessary. Fixing
members wraparound into itself seems more important and once we trigger
vacuums via that it doesn't seem to be too important to have low
settings.

Also, what would be good names? Peter E. complained recently about the
word MultiXactId being exposed in some error messages; maybe "mxid" is
too short an abbreviation of that. Perhaps
multixactid_freeze_min_age = 1 million
multixactid_freeze_table_age = 3 million
?

I personally am fine with mxid - we use xid in other settings after all.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#6)
Re: truncating pg_multixact/members

On Fri, Jan 3, 2014 at 9:11 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas escribió:

On Mon, Dec 30, 2013 at 10:59 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

One problem I see is length of time before freezing multis: they live
for far too long, causing the SLRU files to eat way too much disk space.
I ran burnmulti in a loop, creating multis of 3 members each, with a min
freeze age of 50 million, and this leads to ~770 files in
pg_multixact/offsets and ~2900 files in pg_multixact/members. Each file
is 32 pages long. 256kB apiece. Probably enough to be bothersome.

I think for computing the freezing point for multis, we should slash
min_freeze_age by 10 or something like that. Or just set a hardcoded
one million.

Yeah. Since we expect mxids to be composed at a much lower rate than
xids, we can keep pg_multixact small without needing to increase the
rate of full table scans. However, it seems to me that we ought to
have GUCs for mxid_freeze_table_age and mxid_freeze_min_age. There's
no principled way to derive those values from the corresponding values
for XIDs, and I can't see any reason to suppose that we know how to
auto-tune brand new values better than we know how to auto-tune their
XID equivalents that we've had for years.

One million is probably a reasonable default for mxid_freeze_min_age, though.

I didn't want to propose having new GUCs, but if there's no love for my
idea of deriving it from the Xid freeze policy, I guess it's the only
solution. Just keep in mind we will need to back-patch these new GUCs
to 9.3. Are there objections to this?

Also, what would be good names? Peter E. complained recently about the
word MultiXactId being exposed in some error messages; maybe "mxid" is
too short an abbreviation of that. Perhaps
multixactid_freeze_min_age = 1 million
multixactid_freeze_table_age = 3 million
?
I imagine this stuff would be described somewhere in the docs, perhaps
within the "routine maintenance" section somewhere.

Yeah, this stuff is definitely underdocumented relative to vacuum right now.

As far as back-patching the GUCs, my thought would be to back-patch
them but mark them GUC_NOT_IN_SAMPLE in 9.3, so we don't have to touch
the default postgresql.conf.

Also, while multixactid_freeze_min_age should be low, perhaps a
million as you suggest, multixactid_freeze_table_age should NOT be
lowered to 3 million or anything like it. If you do that, people who
are actually doing lots of row locking will start getting many more
full-table scans. We want to avoid that at all cost. I'd probably
make the default the same as for vacuum_freeze_table_age, so that
mxids only cause extra full-table scans if they're being used more
quickly than xids.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#8)
Re: truncating pg_multixact/members

Robert Haas <robertmhaas@gmail.com> writes:

As far as back-patching the GUCs, my thought would be to back-patch
them but mark them GUC_NOT_IN_SAMPLE in 9.3, so we don't have to touch
the default postgresql.conf.

That seems bizarre and pointless.

Keep in mind that 9.3 is still wet behind the ears and many many people
haven't adopted it yet. If we do what you're suggesting then we're
creating a completely useless inconsistency that will nonetheless affect
all those future adopters ... while accomplishing nothing much for those
who have already installed 9.3. The latter are not going to have these
GUCs in their existing postgresql.conf, true, but there's nothing we can
do about that. (Hint: GUC_NOT_IN_SAMPLE doesn't actually *do* anything,
other than prevent the variable from being shown by SHOW ALL, which is not
exactly helpful here.)

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#10Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#9)
Re: truncating pg_multixact/members

On Sat, Jan 4, 2014 at 12:38 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

As far as back-patching the GUCs, my thought would be to back-patch
them but mark them GUC_NOT_IN_SAMPLE in 9.3, so we don't have to touch
the default postgresql.conf.

That seems bizarre and pointless.

Keep in mind that 9.3 is still wet behind the ears and many many people
haven't adopted it yet. If we do what you're suggesting then we're
creating a completely useless inconsistency that will nonetheless affect
all those future adopters ... while accomplishing nothing much for those
who have already installed 9.3. The latter are not going to have these
GUCs in their existing postgresql.conf, true, but there's nothing we can
do about that. (Hint: GUC_NOT_IN_SAMPLE doesn't actually *do* anything,
other than prevent the variable from being shown by SHOW ALL, which is not
exactly helpful here.)

Well, I guess what I'm really wondering is whether we should refrain
from patching postgresql.conf.sample in 9.3, even if we add the GUC,
just because people may have existing configuration files that they've
already modified, and it could perhaps create confusion.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#10)
Re: truncating pg_multixact/members

Robert Haas <robertmhaas@gmail.com> writes:

On Sat, Jan 4, 2014 at 12:38 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Keep in mind that 9.3 is still wet behind the ears and many many people
haven't adopted it yet. If we do what you're suggesting then we're
creating a completely useless inconsistency that will nonetheless affect
all those future adopters ... while accomplishing nothing much for those
who have already installed 9.3. The latter are not going to have these
GUCs in their existing postgresql.conf, true, but there's nothing we can
do about that. (Hint: GUC_NOT_IN_SAMPLE doesn't actually *do* anything,
other than prevent the variable from being shown by SHOW ALL, which is not
exactly helpful here.)

Well, I guess what I'm really wondering is whether we should refrain
from patching postgresql.conf.sample in 9.3, even if we add the GUC,
just because people may have existing configuration files that they've
already modified, and it could perhaps create confusion.

If we don't update postgresql.conf.sample then we'll just be creating
different confusion. My argument above is that many more people are
likely to be affected in the future by an omission in
postgresql.conf.sample than would be affected now by an inconsistency
between postgresql.conf.sample and their actual conf file.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#11)
Re: truncating pg_multixact/members

On Mon, Jan 6, 2014 at 2:53 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Sat, Jan 4, 2014 at 12:38 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Keep in mind that 9.3 is still wet behind the ears and many many people
haven't adopted it yet. If we do what you're suggesting then we're
creating a completely useless inconsistency that will nonetheless affect
all those future adopters ... while accomplishing nothing much for those
who have already installed 9.3. The latter are not going to have these
GUCs in their existing postgresql.conf, true, but there's nothing we can
do about that. (Hint: GUC_NOT_IN_SAMPLE doesn't actually *do* anything,
other than prevent the variable from being shown by SHOW ALL, which is not
exactly helpful here.)

Well, I guess what I'm really wondering is whether we should refrain
from patching postgresql.conf.sample in 9.3, even if we add the GUC,
just because people may have existing configuration files that they've
already modified, and it could perhaps create confusion.

If we don't update postgresql.conf.sample then we'll just be creating
different confusion. My argument above is that many more people are
likely to be affected in the future by an omission in
postgresql.conf.sample than would be affected now by an inconsistency
between postgresql.conf.sample and their actual conf file.

I don't really have a horse in the race, so I'm OK with that if that's
the consensus.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Jim Nasby
jim@nasby.net
In reply to: Robert Haas (#8)
Re: truncating pg_multixact/members

On 1/4/14, 8:19 AM, Robert Haas wrote:

Also, while multixactid_freeze_min_age should be low, perhaps a
million as you suggest, multixactid_freeze_table_age should NOT be
lowered to 3 million or anything like it. If you do that, people who
are actually doing lots of row locking will start getting many more
full-table scans. We want to avoid that at all cost. I'd probably
make the default the same as for vacuum_freeze_table_age, so that
mxids only cause extra full-table scans if they're being used more
quickly than xids.

Same default as vacuum_freeze_table_age, or default TO vacuum_freeze_table_age? I'm thinking the latter makes more sense...
--
Jim C. Nasby, Data Architect jim@nasby.net
512.569.9461 (cell) http://jim.nasby.net

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Robert Haas
robertmhaas@gmail.com
In reply to: Jim Nasby (#13)
Re: truncating pg_multixact/members

On Mon, Jan 6, 2014 at 7:50 PM, Jim Nasby <jim@nasby.net> wrote:

On 1/4/14, 8:19 AM, Robert Haas wrote:

Also, while multixactid_freeze_min_age should be low, perhaps a
million as you suggest, multixactid_freeze_table_age should NOT be
lowered to 3 million or anything like it. If you do that, people who
are actually doing lots of row locking will start getting many more
full-table scans. We want to avoid that at all cost. I'd probably
make the default the same as for vacuum_freeze_table_age, so that
mxids only cause extra full-table scans if they're being used more
quickly than xids.

Same default as vacuum_freeze_table_age, or default TO
vacuum_freeze_table_age? I'm thinking the latter makes more sense...

Same default. I think it's a mistake to keep leading people to think
that the sensible values for one set of parameters are somehow related
to a sensible set of values for the other set. They're really quite
different things.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Andres Freund
andres@2ndquadrant.com
In reply to: Robert Haas (#14)
Re: truncating pg_multixact/members

On 2014-01-06 20:51:57 -0500, Robert Haas wrote:

On Mon, Jan 6, 2014 at 7:50 PM, Jim Nasby <jim@nasby.net> wrote:

On 1/4/14, 8:19 AM, Robert Haas wrote:

Also, while multixactid_freeze_min_age should be low, perhaps a
million as you suggest, multixactid_freeze_table_age should NOT be
lowered to 3 million or anything like it. If you do that, people who
are actually doing lots of row locking will start getting many more
full-table scans. We want to avoid that at all cost. I'd probably
make the default the same as for vacuum_freeze_table_age, so that
mxids only cause extra full-table scans if they're being used more
quickly than xids.

Same default as vacuum_freeze_table_age, or default TO
vacuum_freeze_table_age? I'm thinking the latter makes more sense...

Same default. I think it's a mistake to keep leading people to think
that the sensible values for one set of parameters are somehow related
to a sensible set of values for the other set. They're really quite
different things.

Valid argument - on the other hand, defaulting to the current variable's
value has the advantage of being less likely to cause pain when doing a
minor version upgrade because suddenly full table vacuums are much more
frequent.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#8)
1 attachment(s)
Re: truncating pg_multixact/members

Robert Haas escribi�:

On Fri, Jan 3, 2014 at 9:11 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Yeah, this stuff is definitely underdocumented relative to vacuum right now.

I have added a paragraph or two. It's a (probably insufficient) start.
I would like to add a sample query to monitor usage, but I just realize
we don't have a function such as age(xid) to expose this info usefully.
We can't introduce one in 9.3 now, but probably we should do so in HEAD.

Also, while multixactid_freeze_min_age should be low, perhaps a
million as you suggest, multixactid_freeze_table_age should NOT be
lowered to 3 million or anything like it. If you do that, people who
are actually doing lots of row locking will start getting many more
full-table scans. We want to avoid that at all cost. I'd probably
make the default the same as for vacuum_freeze_table_age, so that
mxids only cause extra full-table scans if they're being used more
quickly than xids.

I agree that the freeze_table limit should not be low, but 150 million
seems too high. Not really sure what's a good value here.

Here's a first cut at this. Note I have omitted a setting equivalent to
autovacuum_freeze_max_age, but I think we should have one too.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

mxact-freeze-age.patchtext/x-diff; charset=us-asciiDownload
*** a/doc/src/sgml/config.sgml
--- b/doc/src/sgml/config.sgml
***************
*** 5148,5153 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 5148,5168 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-multixact-freeze-table-age" xreflabel="multixact_freeze_table_age">
+       <term><varname>multixact_freeze_table_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>multixact_freeze_table_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         <command>VACUUM</> performs a whole-table scan if the table's
+         <structname>pg_class</>.<structfield>relminmxid</> field has reached
+         the age specified by this setting.  The default is 5 million multixacts.
+         For more information see <xref linkend="multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-vacuum-freeze-min-age" xreflabel="vacuum_freeze_min_age">
        <term><varname>vacuum_freeze_min_age</varname> (<type>integer</type>)</term>
        <indexterm>
***************
*** 5169,5174 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 5184,5205 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-multixact-freeze-min-age" xreflabel="multixact_freeze_min_age">
+       <term><varname>multixact_freeze_min_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>multixact_freeze_min_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         Specifies the cutoff age (in multixacts) that <command>VACUUM</>
+         should use to decide whether to replace multixact IDs with a newer
+         transaction ID or multixact ID while scanning a table.  The default
+         is 1 million multixacts.
+         For more information see <xref linkend="multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-bytea-output" xreflabel="bytea_output">
        <term><varname>bytea_output</varname> (<type>enum</type>)</term>
        <indexterm>
*** a/doc/src/sgml/maintenance.sgml
--- b/doc/src/sgml/maintenance.sgml
***************
*** 599,604 **** HINT:  Stop the postmaster and use a standalone backend to VACUUM in "mydb".
--- 599,632 ----
      page for details about using a single-user backend.
     </para>
  
+    <sect3 id="multixact-wraparound">
+ 	<title>Multixacts and Wraparound</title>
+ 
+ 	<indexterm>
+ 	 <primary>Multixact ID</primary>
+ 	 <secondary>wraparound</secondary>
+ 	</indexterm>
+ 	<!-- how about another index entry primary="wraparound",
+ 		 secondary="multixact", and the same for xids? -->
+ 
+ 	<para>
+ 	 Similar to transaction IDs, Multixact IDs are implemented as a 32-bit
+ 	 counter and corresponding storage which requires careful aging management,
+ 	 storage cleanup, and wraparound handling.  Multixacts are used to implement
+ 	 row locking by multiple transactions: since there is limited space in the
+ 	 tuple header to store lock information, that information is stored separately
+ 	 and only a reference to it is in the tuple header.  As with transaction IDs,
+ 	 <command>VACUUM</> is in charge of removing old values.  Each
+ 	 <command>VACUUM</> run sets a mark in each table that indicates what's the
+ 	 oldest possible value still stored in it; every time this value is older than
+ 	 <xref linkend="guc-multixact-freeze-table-age">, a full-table scan is forced.
+ 	 Any Multixact older than <xref linkend="guc-multixact-freeze-min-age"> is
+ 	 replaced by something else, which can be the zero value, a lone transaction ID,
+ 	 or a newer Multixact.  Eventually, as all tables in all databases have been
+ 	 scanned and their oldest Multixact values are advanced, on-disk storage for
+ 	 Multixact can be removed.
+ 	</para>
+    </sect3>
    </sect2>
  
    <sect2 id="autovacuum">
*** a/src/backend/commands/vacuum.c
--- b/src/backend/commands/vacuum.c
***************
*** 55,60 ****
--- 55,62 ----
   */
  int			vacuum_freeze_min_age;
  int			vacuum_freeze_table_age;
+ int			multixact_freeze_min_age;
+ int			multixact_freeze_table_age;
  
  
  /* A few variables that don't seem worth passing around as parameters */
***************
*** 406,411 **** vacuum_set_xid_limits(int freeze_min_age,
--- 408,414 ----
  					  MultiXactId *mxactFullScanLimit)
  {
  	int			freezemin;
+ 	int			mxid_freezemin;
  	TransactionId limit;
  	TransactionId safeLimit;
  	MultiXactId	mxactLimit;
***************
*** 462,472 **** vacuum_set_xid_limits(int freeze_min_age,
  	*freezeLimit = limit;
  
  	/*
! 	 * simplistic MultiXactId removal limit: use the same policy as for
! 	 * freezing Xids (except we use the oldest known mxact instead of the
! 	 * current next value).
  	 */
! 	mxactLimit = GetOldestMultiXactId() - freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
  	*multiXactCutoff = mxactLimit;
--- 465,475 ----
  	*freezeLimit = limit;
  
  	/*
! 	 * Determine the minimum multixact freeze age to use: as specified by
! 	 * caller, or multixact_freeze_min_age.
  	 */
! 	mxid_freezemin = Min(freeze_min_age, multixact_freeze_min_age);
! 	mxactLimit = GetOldestMultiXactId() - mxid_freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
  	*multiXactCutoff = mxactLimit;
***************
*** 503,516 **** vacuum_set_xid_limits(int freeze_min_age,
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * (and limits) from plain XIDs here.
  		 */
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
  			mxactLimit = FirstMultiXactId;
  
  		*mxactFullScanLimit = mxactLimit;
  	}
  }
  
  /*
--- 506,524 ----
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * from plain XIDs here.
  		 */
+ 		freezetable = multixact_freeze_table_age;
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
  			mxactLimit = FirstMultiXactId;
  
  		*mxactFullScanLimit = mxactLimit;
  	}
+ 	else
+ 	{
+ 		Assert(mxactFullScanLimit == NULL);
+ 	}
  }
  
  /*
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
***************
*** 1907,1912 **** static struct config_int ConfigureNamesInt[] =
--- 1907,1932 ----
  	},
  
  	{
+ 		{"multixact_freeze_min_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Minimum age at which VACUUM should freeze a MultiXactId in a table row."),
+ 			NULL
+ 		},
+ 		&multixact_freeze_min_age,
+ 		1000000, 0, 200000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
+ 		{"multixact_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."),
+ 			NULL
+ 		},
+ 		&multixact_freeze_table_age,
+ 		5000000, 0, 200000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
  		{"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_MASTER,
  			gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."),
  			NULL
*** a/src/backend/utils/misc/postgresql.conf.sample
--- b/src/backend/utils/misc/postgresql.conf.sample
***************
*** 492,497 ****
--- 492,499 ----
  #lock_timeout = 0			# in milliseconds, 0 is disabled
  #vacuum_freeze_min_age = 50000000
  #vacuum_freeze_table_age = 150000000
+ #multixact_freeze_min_age = 1000000
+ #multixact_freeze_table_age = 5000000
  #bytea_output = 'hex'			# hex, escape
  #xmlbinary = 'base64'
  #xmloption = 'content'
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
***************
*** 136,141 **** extern PGDLLIMPORT int default_statistics_target;		/* PGDLLIMPORT for
--- 136,143 ----
  														 * PostGIS */
  extern int	vacuum_freeze_min_age;
  extern int	vacuum_freeze_table_age;
+ extern int	multixact_freeze_min_age;
+ extern int	multixact_freeze_table_age;
  
  
  /* in commands/vacuum.c */
#17Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#16)
Re: truncating pg_multixact/members

Alvaro Herrera escribi�:

Here's a first cut at this. Note I have omitted a setting equivalent to
autovacuum_freeze_max_age, but I think we should have one too.

Some more comments on the patch:

* I haven't introduced settings to tweak this per table for
autovacuum. I don't think those are needed. It's not hard to do,
however; if people opine against this, I will implement that.

* The multixact_freeze_table_age value has been set to 5 million.
I feel this is a big enough number that shouldn't cause too much
vacuuming churn, while at the same time not leaving excessive storage
occupied by pg_multixact/members, which amplifies the space used by the
average number of member in each multi.

(A bit of math: each Xid uses 2 bits. Therefore for the default 200
million transactions of vacuum_freeze_table_age we use 50 million bytes,
or about 27 MB of space, plus some room for per-page LSNs. For each
Multi we use 4 bytes in offset plus 5 bytes per member; if we consider 2
members per multi in average, that totals 70 million bytes for the
default multixact_freeze_table_age, so 66 MB of space.)

* I have named the parameters by simply replacing "vacuum" with
"multixact". I could instead have added the "multixact" word in the
middle:
vacuum_multixact_freeze_min_age
but this doesn't seem an improvement.

* In the word "Multixact" in the docs I left the X as lowercase. I used
uppercase first but that looked pretty odd. In the middle of a
sentence, the M is also lowercase.

I reworded the paragraph in maintenance.sgml a bit. If there are
suggestions, please shout.

<para>
Similar to transaction IDs, Multixact IDs are implemented as a 32-bit
counter and corresponding storage which requires careful aging management,
storage cleanup, and wraparound handling. Multixacts are used to implement
row locking by multiple transactions: since there is limited space in the
tuple header to store lock information, that information is stored separately
and only a reference to it is in the <structfield>xmax</> field in the tuple
header.
</para>

<para>
As with transaction IDs,
<command>VACUUM</> is in charge of removing old values. Each
<command>VACUUM</> run sets <structname>pg_class</>.<structfield>relminmxid</>
indicating the oldest possible value still stored in that table; every time
this value is older than <xref linkend="guc-multixact-freeze-table-age">, a
full-table scan is forced.
During any table scan (either partial or full-table), any multixact older
than <xref linkend="guc-multixact-freeze-min-age"> is replaced by something
else, which can be the zero value, a single transaction ID,
or a newer multixact. Eventually, as all tables in all databases are
scanned and their oldest multixact values are advanced, on-disk storage for
older multixacts can be removed.
</para>

</sect3>

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Andres Freund
andres@2ndquadrant.com
In reply to: Alvaro Herrera (#17)
Re: truncating pg_multixact/members

Hi,

On 2014-01-20 15:39:33 -0300, Alvaro Herrera wrote:

* The multixact_freeze_table_age value has been set to 5 million.
I feel this is a big enough number that shouldn't cause too much
vacuuming churn, while at the same time not leaving excessive storage
occupied by pg_multixact/members, which amplifies the space used by the
average number of member in each multi.

That seems to be *far* too low to me. In some workloads, remember we've
seen pg_controldata outputs with far high next multi than next xid, that
will cause excessive full table scans. I really think that we shouldn't
change the default for freeze_table_age for multis at all.
I think we should have a lower value for the vacuum_freeze_min_age
equivalent, but that's it.

(A bit of math: each Xid uses 2 bits. Therefore for the default 200
million transactions of vacuum_freeze_table_age we use 50 million bytes,
or about 27 MB of space, plus some room for per-page LSNs. For each
Multi we use 4 bytes in offset plus 5 bytes per member; if we consider 2
members per multi in average, that totals 70 million bytes for the
default multixact_freeze_table_age, so 66 MB of space.)

That doesn't seem sufficient cause to change the default to me.

* I have named the parameters by simply replacing "vacuum" with
"multixact". I could instead have added the "multixact" word in the
middle:
vacuum_multixact_freeze_min_age
but this doesn't seem an improvement.

I vote for the longer version. Right now you can get all relevant vacuum
parameters by grepping/searching for vacuum, we shouldn't give up on
that. If we consider vacuum_multixact_freeze_min_age to be too long, I'd
rather vote for replacing multixact by mxid or such.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#17)
Re: truncating pg_multixact/members

On Mon, Jan 20, 2014 at 1:39 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

* I haven't introduced settings to tweak this per table for
autovacuum. I don't think those are needed. It's not hard to do,
however; if people opine against this, I will implement that.

I can't think of any reason to believe that it will be less important
to tune these values on a per-table basis than it is to be able to do
the same with the autovacuum parameters. Indeed, all the discussion
on this thread suggests precisely that we have no real idea how to set
these values yet, so more configurability is good. Even if you reject
that argument, I think it's a bad idea to start making xmax vacuuming
and xmin vacuuming less than parallel; such decisions confuse users.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#19)
1 attachment(s)
Re: truncating pg_multixact/members

Robert Haas escribi�:

On Mon, Jan 20, 2014 at 1:39 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

* I haven't introduced settings to tweak this per table for
autovacuum. I don't think those are needed. It's not hard to do,
however; if people opine against this, I will implement that.

I can't think of any reason to believe that it will be less important
to tune these values on a per-table basis than it is to be able to do
the same with the autovacuum parameters. Indeed, all the discussion
on this thread suggests precisely that we have no real idea how to set
these values yet, so more configurability is good. Even if you reject
that argument, I think it's a bad idea to start making xmax vacuuming
and xmin vacuuming less than parallel; such decisions confuse users.

Yeah, I can relate to this argument. I have added per-table
configurability to this, and also added the an equivalent of
autovacuum_freeze_max_age to force a for-wraparound full scan of a table
based on multixacts.

I haven't really tested this beyond ensuring that it compiles, and I
haven't changed the default values, but here it is in case someone wants
to have a look and comment --- particularly on the doc additions.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

mxact-freeze-age-2.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index efac9eb..9e4e979 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4730,6 +4730,33 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
+      <term><varname>autovacuum_multixact_freeze_max_age</varname> (<type>integer</type>)</term>
+      <indexterm>
+       <primary><varname>autovacuum_multixact_freeze_max_age</varname> configuration parameter</primary>
+      </indexterm>
+      <listitem>
+       <para>
+        Specifies the maximum age (in Multixacts) that a table's
+        <structname>pg_class</>.<structfield>relminmxid</> field can
+        attain before a <command>VACUUM</> operation is forced to
+        prevent Multixact ID wraparound within the table.
+        Note that the system will launch autovacuum processes to
+        prevent wraparound even when autovacuum is otherwise disabled.
+       </para>
+
+       <para>
+        Vacuum also allows removal of old files from the
+        <filename>pg_multixact/members</> and <filename>pg_multixact/offsets</>
+        subdirectories, which is why the default is a relatively low
+        50 million transactions.
+        This parameter can only be set at server start, but the setting
+        can be reduced for individual tables by changing storage parameters.
+        For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
       <term><varname>autovacuum_vacuum_cost_delay</varname> (<type>integer</type>)</term>
       <indexterm>
@@ -5148,6 +5175,21 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-vacuum-multixact-freeze-table-age" xreflabel="vacuum_multixact_freeze_table_age">
+      <term><varname>vacuum_multixact_freeze_table_age</varname> (<type>integer</type>)</term>
+      <indexterm>
+       <primary><varname>vacuum_multixact_freeze_table_age</> configuration parameter</primary>
+      </indexterm>
+      <listitem>
+       <para>
+        <command>VACUUM</> performs a whole-table scan if the table's
+        <structname>pg_class</>.<structfield>relminmxid</> field has reached
+        the age specified by this setting.  The default is 5 million multixacts.
+        For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-vacuum-freeze-min-age" xreflabel="vacuum_freeze_min_age">
       <term><varname>vacuum_freeze_min_age</varname> (<type>integer</type>)</term>
       <indexterm>
@@ -5169,6 +5211,22 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-vacuum-multixact-freeze-min-age" xreflabel="vacuum_multixact_freeze_min_age">
+      <term><varname>vacuum_multixact_freeze_min_age</varname> (<type>integer</type>)</term>
+      <indexterm>
+       <primary><varname>vacuum_multixact_freeze_min_age</> configuration parameter</primary>
+      </indexterm>
+      <listitem>
+       <para>
+        Specifies the cutoff age (in multixacts) that <command>VACUUM</>
+        should use to decide whether to replace multixact IDs with a newer
+        transaction ID or multixact ID while scanning a table.  The default
+        is 1 million multixacts.
+        For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-bytea-output" xreflabel="bytea_output">
       <term><varname>bytea_output</varname> (<type>enum</type>)</term>
       <indexterm>
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index ae6456b..7130cd8 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -108,7 +108,8 @@
 
      <listitem>
       <simpara>To protect against loss of very old data due to
-      <firstterm>transaction ID wraparound</>.</simpara>
+      <firstterm>transaction ID wraparound</> or
+      <firstterm>Multixact ID wraparound</>.</simpara>
      </listitem>
     </orderedlist>
 
@@ -599,6 +600,41 @@ HINT:  Stop the postmaster and use a standalone backend to VACUUM in "mydb".
     page for details about using a single-user backend.
    </para>
 
+   <sect3 id="vacuum-for-multixact-wraparound">
+    <title>Multixacts and Wraparound</title>
+
+    <indexterm>
+     <primary>Multixact ID</primary>
+     <secondary>wraparound</secondary>
+    </indexterm>
+    <!-- how about another index entry primary="wraparound",
+         secondary="multixact", and the same for xids? -->
+
+    <para>
+     Similar to transaction IDs, Multixact IDs are implemented as a 32-bit
+     counter and corresponding storage which requires careful aging management,
+     storage cleanup, and wraparound handling.  Multixacts are used to implement
+     row locking by multiple transactions: since there is limited space in the
+     tuple header to store lock information, that information is stored separately
+     and only a reference to it is in the <structfield>xmax</> field in the tuple
+     header.
+    </para>
+
+    <para>
+     As with transaction IDs, <command>VACUUM</> is in charge of removing old
+     values.  Each <command>VACUUM</> run sets
+     <structname>pg_class</>.<structfield>relminmxid</> indicating the oldest
+     possible value still stored in that table; every time this value is older
+     than <xref linkend="guc-vacuum-multixact-freeze-table-age">, a full-table
+     scan is forced.  During a table scan, either partial or full-table, any
+     multixact older than <xref linkend="guc-vacuum-multixact-freeze-min-age">
+     is replaced by a different value, which can be the zero value, a single
+     transaction ID, or a newer multixact; full-table scans enable advancing
+     the value for that table.  Eventually, as all tables in all databases are
+     scanned and their oldest multixact values are advanced, on-disk storage
+     for older multixacts can be removed.
+    </para>
+   </sect3>
   </sect2>
 
   <sect2 id="autovacuum">
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 26eca67..38134dd 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -981,7 +981,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
      <para>
      Custom <xref linkend="guc-vacuum-freeze-min-age"> parameter. Note that
      autovacuum will ignore attempts to set a per-table
-     <literal>autovacuum_freeze_min_age</> larger than the half system-wide
+     <literal>autovacuum_freeze_min_age</> larger than half the system-wide
      <xref linkend="guc-autovacuum-freeze-max-age"> setting.
      </para>
     </listitem>
@@ -1010,6 +1010,43 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)</term>
+    <listitem>
+     <para>
+      Custom <xref linkend="guc-vacuum-multixact-freeze-min-age"> parameter.
+      Note that autovacuum will ignore attempts to set a per-table
+      <literal>autovacuum_multixact_freeze_min_age</> larger than half the
+      system-wide <xref linkend="guc-autovacuum-multixact-freeze-max-age">
+      setting.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)</term>
+    <listitem>
+     <para>
+      Custom <xref linkend="guc-autovacuum-multixact-freeze-max-age"> parameter. Note
+      that autovacuum will ignore attempts to set a per-table
+      <literal>autovacuum_multixact_freeze_max_age</> larger than the
+      system-wide setting (it can only be set smaller).  Note that while you
+      can set <literal>autovacuum_multixact_freeze_max_age</> very small,
+      or even zero, this is usually unwise since it will force frequent
+      vacuuming.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)</term>
+    <listitem>
+     <para>
+      Custom <xref linkend="guc-vacuum-multixact-freeze-table-age"> parameter.
+     </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
 
   </refsect2>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index c439702..d7c3e11 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -164,6 +164,14 @@ static relopt_int intRelOpts[] =
 	},
 	{
 		{
+			"autovacuum_multixact_freeze_min_age",
+			"Minimum MultiXact age at which VACUUM should freeze a row MultiXact's, for autovacuum",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+		},
+		-1, 0, 1000000000
+	},
+	{
+		{
 			"autovacuum_freeze_max_age",
 			"Age at which to autovacuum a table to prevent transaction ID wraparound",
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
@@ -172,11 +180,26 @@ static relopt_int intRelOpts[] =
 	},
 	{
 		{
+			"autovacuum_multixact_freeze_max_age",
+			"MultiXact age at which to autovacuum a table to prevent MultiXact wraparound",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+		},
+		-1, 100000000, 2000000000
+	},
+	{
+		{
 			"autovacuum_freeze_table_age",
 			"Age at which VACUUM should perform a full table sweep to replace old Xid values with FrozenXID",
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
 		}, -1, 0, 2000000000
 	},
+	{
+		{
+			"autovacuum_multixact_freeze_table_age",
+			"Age of MultiXact at which VACUUM should perform a full table sweep to replace old MultiXact values with newer ones",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+		}, -1, 0, 2000000000
+	},
 	/* list terminator */
 	{{NULL}}
 };
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 1cdb220..c903f0e 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -64,9 +64,13 @@ typedef struct
 
 
 static void rebuild_relation(Relation OldHeap, Oid indexOid,
-				 int freeze_min_age, int freeze_table_age, bool verbose);
+				 int freeze_min_age, int freeze_table_age,
+				 int multixact_freeze_min_age, int multixact_freeze_table_age,
+				 bool verbose);
 static void copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
-			   int freeze_min_age, int freeze_table_age, bool verbose,
+			   int freeze_min_age, int freeze_table_age,
+			   int multixact_freeze_min_age, int multixact_freeze_table_age,
+			   bool verbose,
 			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
 			   MultiXactId *pCutoffMulti);
 static List *get_tables_to_cluster(MemoryContext cluster_context);
@@ -179,7 +183,7 @@ cluster(ClusterStmt *stmt, bool isTopLevel)
 		 * Do the job.  We use a -1 freeze_min_age to avoid having CLUSTER
 		 * freeze tuples earlier than a plain VACUUM would.
 		 */
-		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1);
+		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1, -1, -1);
 	}
 	else
 	{
@@ -230,7 +234,7 @@ cluster(ClusterStmt *stmt, bool isTopLevel)
 			PushActiveSnapshot(GetTransactionSnapshot());
 			/* Do the job.  As above, use a -1 freeze_min_age. */
 			cluster_rel(rvtc->tableOid, rvtc->indexOid, true, stmt->verbose,
-						-1, -1);
+						-1, -1, -1, -1);
 			PopActiveSnapshot();
 			CommitTransactionCommand();
 		}
@@ -262,7 +266,8 @@ cluster(ClusterStmt *stmt, bool isTopLevel)
  */
 void
 cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
-			int freeze_min_age, int freeze_table_age)
+			int freeze_min_age, int freeze_table_age,
+			int multixact_freeze_min_age, int multixact_freeze_table_age)
 {
 	Relation	OldHeap;
 
@@ -407,6 +412,7 @@ cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
 
 	/* rebuild_relation does all the dirty work */
 	rebuild_relation(OldHeap, indexOid, freeze_min_age, freeze_table_age,
+					 multixact_freeze_min_age, multixact_freeze_table_age,
 					 verbose);
 
 	/* NB: rebuild_relation does heap_close() on OldHeap */
@@ -566,7 +572,9 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
  */
 static void
 rebuild_relation(Relation OldHeap, Oid indexOid,
-				 int freeze_min_age, int freeze_table_age, bool verbose)
+				 int freeze_min_age, int freeze_table_age,
+				 int multixact_freeze_min_age, int multixact_freeze_table_age,
+				 bool verbose)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
@@ -591,7 +599,9 @@ rebuild_relation(Relation OldHeap, Oid indexOid,
 
 	/* Copy the heap data into the new table in the desired order */
 	copy_heap_data(OIDNewHeap, tableOid, indexOid,
-				   freeze_min_age, freeze_table_age, verbose,
+				   freeze_min_age, freeze_table_age,
+				   multixact_freeze_min_age, multixact_freeze_table_age,
+				   verbose,
 				   &swap_toast_by_content, &frozenXid, &cutoffMulti);
 
 	/*
@@ -733,7 +743,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace)
  */
 static void
 copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
-			   int freeze_min_age, int freeze_table_age, bool verbose,
+			   int freeze_min_age, int freeze_table_age,
+			   int multixact_freeze_min_age, int multixact_freeze_table_age,
+			   bool verbose,
 			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
 			   MultiXactId *pCutoffMulti)
 {
@@ -849,6 +861,7 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
 	 * compute xids used to freeze and weed out dead tuples.
 	 */
 	vacuum_set_xid_limits(freeze_min_age, freeze_table_age,
+						  multixact_freeze_min_age, multixact_freeze_table_age,
 						  OldHeap->rd_rel->relisshared,
 						  &OldestXmin, &FreezeXid, NULL, &MultiXactCutoff,
 						  NULL);
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index d50333f..b530363 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -55,6 +55,8 @@
  */
 int			vacuum_freeze_min_age;
 int			vacuum_freeze_table_age;
+int			vacuum_multixact_freeze_min_age;
+int			vacuum_multixact_freeze_table_age;
 
 
 /* A few variables that don't seem worth passing around as parameters */
@@ -398,6 +400,8 @@ get_rel_oids(Oid relid, const RangeVar *vacrel)
 void
 vacuum_set_xid_limits(int freeze_min_age,
 					  int freeze_table_age,
+					  int multixact_freeze_min_age,
+					  int multixact_freeze_table_age,
 					  bool sharedRel,
 					  TransactionId *oldestXmin,
 					  TransactionId *freezeLimit,
@@ -406,9 +410,11 @@ vacuum_set_xid_limits(int freeze_min_age,
 					  MultiXactId *mxactFullScanLimit)
 {
 	int			freezemin;
+	int			mxid_freezemin;
 	TransactionId limit;
 	TransactionId safeLimit;
 	MultiXactId	mxactLimit;
+	MultiXactId	safeMxactLimit;
 
 	/*
 	 * We can always ignore processes running lazy vacuum.	This is because we
@@ -462,13 +468,36 @@ vacuum_set_xid_limits(int freeze_min_age,
 	*freezeLimit = limit;
 
 	/*
-	 * simplistic MultiXactId removal limit: use the same policy as for
-	 * freezing Xids (except we use the oldest known mxact instead of the
-	 * current next value).
+	 * Determine the minimum multixact freeze age to use: as specified by
+	 * caller, or vacuum_multixact_freeze_min_age, but in any case not more
+	 * than half autovacuum_multixact_freeze_max_age, so that autovacuums to
+	 * prevent MultiXact wraparound won't occur too frequently.
 	 */
-	mxactLimit = GetOldestMultiXactId() - freezemin;
+	mxid_freezemin = multixact_freeze_min_age;
+	if (mxid_freezemin < 0)
+		mxid_freezemin = vacuum_multixact_freeze_min_age;
+	mxid_freezemin = Min(mxid_freezemin,
+						 autovacuum_multixact_freeze_max_age / 2);
+	Assert(mxid_freezemin >= 0);
+
+	/* compute the cutoff multi, being careful to generate a valid value */
+	mxactLimit = GetOldestMultiXactId() - mxid_freezemin;
 	if (mxactLimit < FirstMultiXactId)
 		mxactLimit = FirstMultiXactId;
+
+	safeMxactLimit =
+		ReadNextMultiXactId() - autovacuum_multixact_freeze_max_age;
+	if (safeMxactLimit < FirstMultiXactId)
+		safeMxactLimit = FirstMultiXactId;
+
+	if (MultiXactIdPrecedes(mxactLimit, safeMxactLimit))
+	{
+		ereport(WARNING,
+				(errmsg("oldest MultiXact is far in the past"),
+				 errhint("Close open transactions with MultiXacts soon to avoid wraparound problems.")));
+		mxactLimit = safeMxactLimit;
+	}
+
 	*multiXactCutoff = mxactLimit;
 
 	if (xidFullScanLimit != NULL)
@@ -503,7 +532,17 @@ vacuum_set_xid_limits(int freeze_min_age,
 		/*
 		 * Compute MultiXactId limit to cause a full-table vacuum, being
 		 * careful not to generate an invalid multi. We just copy the logic
-		 * (and limits) from plain XIDs here.
+		 * from plain XIDs here.
+		 */
+		freezetable = multixact_freeze_table_age;
+		if (freezetable < 0)
+			freezetable = vacuum_multixact_freeze_table_age;
+		freezetable = Min(freezetable,
+						  autovacuum_multixact_freeze_max_age * 0.95);
+		Assert(freezetable >= 0);
+
+		/* Compute MultiXact limit causing a full-table vacuum, being careful
+		 * to generate a valid MultiXact value.
 		 */
 		mxactLimit = ReadNextMultiXactId() - freezetable;
 		if (mxactLimit < FirstMultiXactId)
@@ -511,6 +550,10 @@ vacuum_set_xid_limits(int freeze_min_age,
 
 		*mxactFullScanLimit = mxactLimit;
 	}
+	else
+	{
+		Assert(mxactFullScanLimit == NULL);
+	}
 }
 
 /*
@@ -1150,7 +1193,9 @@ vacuum_rel(Oid relid, VacuumStmt *vacstmt, bool do_toast, bool for_wraparound)
 		/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
 		cluster_rel(relid, InvalidOid, false,
 					(vacstmt->options & VACOPT_VERBOSE) != 0,
-					vacstmt->freeze_min_age, vacstmt->freeze_table_age);
+					vacstmt->freeze_min_age, vacstmt->freeze_table_age,
+					vacstmt->multixact_freeze_min_age,
+					vacstmt->multixact_freeze_table_age);
 	}
 	else
 		lazy_vacuum_rel(onerel, vacstmt, vac_strategy);
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c
index 402a9e7..9f70def 100644
--- a/src/backend/commands/vacuumlazy.c
+++ b/src/backend/commands/vacuumlazy.c
@@ -203,6 +203,8 @@ lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt,
 	vac_strategy = bstrategy;
 
 	vacuum_set_xid_limits(vacstmt->freeze_min_age, vacstmt->freeze_table_age,
+						  vacstmt->multixact_freeze_min_age,
+						  vacstmt->multixact_freeze_table_age,
 						  onerel->rd_rel->relisshared,
 						  &OldestXmin, &FreezeLimit, &xidFullScanLimit,
 						  &MultiXactCutoff, &mxactFullScanLimit);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3feced6..ed22e46 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8474,6 +8474,8 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_VERBOSE;
 					n->freeze_min_age = $3 ? 0 : -1;
 					n->freeze_table_age = $3 ? 0 : -1;
+					n->multixact_freeze_min_age = $3 ? 0 : -1;
+					n->multixact_freeze_table_age = $3 ? 0 : -1;
 					n->relation = NULL;
 					n->va_cols = NIL;
 					$$ = (Node *)n;
@@ -8488,6 +8490,8 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_VERBOSE;
 					n->freeze_min_age = $3 ? 0 : -1;
 					n->freeze_table_age = $3 ? 0 : -1;
+					n->multixact_freeze_min_age = $3 ? 0 : -1;
+					n->multixact_freeze_table_age = $3 ? 0 : -1;
 					n->relation = $5;
 					n->va_cols = NIL;
 					$$ = (Node *)n;
@@ -8502,6 +8506,8 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_VERBOSE;
 					n->freeze_min_age = $3 ? 0 : -1;
 					n->freeze_table_age = $3 ? 0 : -1;
+					n->multixact_freeze_min_age = $3 ? 0 : -1;
+					n->multixact_freeze_table_age = $3 ? 0 : -1;
 					$$ = (Node *)n;
 				}
 			| VACUUM '(' vacuum_option_list ')'
@@ -8509,9 +8515,17 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM | $3;
 					if (n->options & VACOPT_FREEZE)
+					{
 						n->freeze_min_age = n->freeze_table_age = 0;
+						n->multixact_freeze_min_age = 0;
+						n->multixact_freeze_table_age = 0;
+					}
 					else
+					{
 						n->freeze_min_age = n->freeze_table_age = -1;
+						n->multixact_freeze_min_age = -1;
+						n->multixact_freeze_table_age = -1;
+					}
 					n->relation = NULL;
 					n->va_cols = NIL;
 					$$ = (Node *) n;
@@ -8521,9 +8535,17 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM | $3;
 					if (n->options & VACOPT_FREEZE)
+					{
 						n->freeze_min_age = n->freeze_table_age = 0;
+						n->multixact_freeze_min_age = 0;
+						n->multixact_freeze_table_age = 0;
+					}
 					else
+					{
 						n->freeze_min_age = n->freeze_table_age = -1;
+						n->multixact_freeze_min_age = -1;
+						n->multixact_freeze_table_age = -1;
+					}
 					n->relation = $5;
 					n->va_cols = $6;
 					if (n->va_cols != NIL)	/* implies analyze */
@@ -8553,6 +8575,8 @@ AnalyzeStmt:
 						n->options |= VACOPT_VERBOSE;
 					n->freeze_min_age = -1;
 					n->freeze_table_age = -1;
+					n->multixact_freeze_min_age = -1;
+					n->multixact_freeze_table_age = -1;
 					n->relation = NULL;
 					n->va_cols = NIL;
 					$$ = (Node *)n;
@@ -8565,6 +8589,8 @@ AnalyzeStmt:
 						n->options |= VACOPT_VERBOSE;
 					n->freeze_min_age = -1;
 					n->freeze_table_age = -1;
+					n->multixact_freeze_min_age = -1;
+					n->multixact_freeze_table_age = -1;
 					n->relation = $3;
 					n->va_cols = $4;
 					$$ = (Node *)n;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 38e206d..a6b89f8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -116,6 +116,7 @@ double		autovacuum_vac_scale;
 int			autovacuum_anl_thresh;
 double		autovacuum_anl_scale;
 int			autovacuum_freeze_max_age;
+int			autovacuum_multixact_freeze_max_age;
 
 int			autovacuum_vac_cost_delay;
 int			autovacuum_vac_cost_limit;
@@ -144,6 +145,8 @@ static MultiXactId recentMulti;
 /* Default freeze ages to use for autovacuum (varies by database) */
 static int	default_freeze_min_age;
 static int	default_freeze_table_age;
+static int	default_multixact_freeze_min_age;
+static int	default_multixact_freeze_table_age;
 
 /* Memory context for long-lived data */
 static MemoryContext AutovacMemCxt;
@@ -185,6 +188,8 @@ typedef struct autovac_table
 	bool		at_doanalyze;
 	int			at_freeze_min_age;
 	int			at_freeze_table_age;
+	int			at_multixact_freeze_min_age;
+	int			at_multixact_freeze_table_age;
 	int			at_vacuum_cost_delay;
 	int			at_vacuum_cost_limit;
 	bool		at_wraparound;
@@ -1129,7 +1134,7 @@ do_start_worker(void)
 
 	/* Also determine the oldest datminmxid we will consider. */
 	recentMulti = ReadNextMultiXactId();
-	multiForceLimit = recentMulti - autovacuum_freeze_max_age;
+	multiForceLimit = recentMulti - autovacuum_multixact_freeze_max_age;
 	if (multiForceLimit < FirstMultiXactId)
 		multiForceLimit -= FirstMultiXactId;
 
@@ -1955,11 +1960,15 @@ do_autovacuum(void)
 	{
 		default_freeze_min_age = 0;
 		default_freeze_table_age = 0;
+		default_multixact_freeze_min_age = 0;
+		default_multixact_freeze_table_age = 0;
 	}
 	else
 	{
 		default_freeze_min_age = vacuum_freeze_min_age;
 		default_freeze_table_age = vacuum_freeze_table_age;
+		default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age;
+		default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
 	}
 
 	ReleaseSysCache(tuple);
@@ -2510,6 +2519,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 	{
 		int			freeze_min_age;
 		int			freeze_table_age;
+		int			multixact_freeze_min_age;
+		int			multixact_freeze_table_age;
 		int			vac_cost_limit;
 		int			vac_cost_delay;
 
@@ -2543,12 +2554,22 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 			? avopts->freeze_table_age
 			: default_freeze_table_age;
 
+		multixact_freeze_min_age = (avopts && avopts->multixact_freeze_min_age >= 0)
+			? avopts->multixact_freeze_min_age
+			: default_multixact_freeze_min_age;
+
+		multixact_freeze_table_age = (avopts && avopts->multixact_freeze_table_age >= 0)
+			? avopts->multixact_freeze_table_age
+			: default_multixact_freeze_table_age;
+
 		tab = palloc(sizeof(autovac_table));
 		tab->at_relid = relid;
 		tab->at_dovacuum = dovacuum;
 		tab->at_doanalyze = doanalyze;
 		tab->at_freeze_min_age = freeze_min_age;
 		tab->at_freeze_table_age = freeze_table_age;
+		tab->at_multixact_freeze_min_age = multixact_freeze_min_age;
+		tab->at_multixact_freeze_table_age = multixact_freeze_table_age;
 		tab->at_vacuum_cost_limit = vac_cost_limit;
 		tab->at_vacuum_cost_delay = vac_cost_delay;
 		tab->at_wraparound = wraparound;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a055231..72cf3af 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1907,6 +1907,26 @@ static struct config_int ConfigureNamesInt[] =
 	},
 
 	{
+		{"vacuum_multixact_freeze_min_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Minimum age at which VACUUM should freeze a MultiXactId in a table row."),
+			NULL
+		},
+		&vacuum_multixact_freeze_min_age,
+		1000000, 0, 200000000,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"vacuum_multixact_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."),
+			NULL
+		},
+		&vacuum_multixact_freeze_table_age,
+		5000000, 0, 200000000,
+		NULL, NULL, NULL
+	},
+
+	{
 		{"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_MASTER,
 			gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."),
 			NULL
@@ -2296,6 +2316,16 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 	{
+		/* ditto */
+		{"autovacuum_multixact_freeze_max_age", PGC_POSTMASTER, AUTOVACUUM,
+			gettext_noop("MultiXact age at which to autovacuum a table to prevent MultiXact wraparound."),
+			NULL
+		},
+		&autovacuum_multixact_freeze_max_age,
+		200000000, 100000000, 2000000000,
+		NULL, NULL, NULL
+	},
+	{
 		/* see max_connections */
 		{"autovacuum_max_workers", PGC_POSTMASTER, AUTOVACUUM,
 			gettext_noop("Sets the maximum number of simultaneously running autovacuum worker processes."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 0303ac7..28da736 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -362,12 +362,12 @@
 					#   panic
 
 #log_min_error_statement = error	# values in order of decreasing detail:
-				 	#   debug5
+					#   debug5
 					#   debug4
 					#   debug3
 					#   debug2
 					#   debug1
-				 	#   info
+					#   info
 					#   notice
 					#   warning
 					#   error
@@ -431,7 +431,7 @@
 #track_counts = on
 #track_io_timing = off
 #track_functions = none			# none, pl, all
-#track_activity_query_size = 1024 	# (change requires restart)
+#track_activity_query_size = 1024	# (change requires restart)
 #update_process_title = on
 #stats_temp_directory = 'pg_stat_tmp'
 
@@ -465,6 +465,9 @@
 #autovacuum_analyze_scale_factor = 0.1	# fraction of table size before analyze
 #autovacuum_freeze_max_age = 200000000	# maximum XID age before forced vacuum
 					# (change requires restart)
+#autovacuum_multixact_freeze_max_age = 200000000	# maximum Multixact age
+					# before forced vacuum
+					# (change requires restart)
 #autovacuum_vacuum_cost_delay = 20ms	# default vacuum cost delay for
 					# autovacuum, in milliseconds;
 					# -1 means use vacuum_cost_delay
@@ -492,6 +495,8 @@
 #lock_timeout = 0			# in milliseconds, 0 is disabled
 #vacuum_freeze_min_age = 50000000
 #vacuum_freeze_table_age = 150000000
+#vacuum_multixact_freeze_min_age = 1000000
+#vacuum_multixact_freeze_table_age = 5000000
 #bytea_output = 'hex'			# hex, escape
 #xmlbinary = 'base64'
 #xmloption = 'content'
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 52ca1d1..9b0a530 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -20,7 +20,8 @@
 
 extern void cluster(ClusterStmt *stmt, bool isTopLevel);
 extern void cluster_rel(Oid tableOid, Oid indexOid, bool recheck,
-			bool verbose, int freeze_min_age, int freeze_table_age);
+			bool verbose, int freeze_min_age, int freeze_table_age,
+			int multixact_freeze_min_age, int multixact_freeze_table_age);
 extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 						   bool recheck, LOCKMODE lockmode);
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 44a3c3b..4bc0f14 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -136,6 +136,8 @@ extern PGDLLIMPORT int default_statistics_target;		/* PGDLLIMPORT for
 														 * PostGIS */
 extern int	vacuum_freeze_min_age;
 extern int	vacuum_freeze_table_age;
+extern int	vacuum_multixact_freeze_min_age;
+extern int	vacuum_multixact_freeze_table_age;
 
 
 /* in commands/vacuum.c */
@@ -156,6 +158,8 @@ extern void vac_update_relstats(Relation relation,
 					TransactionId frozenxid,
 					MultiXactId minmulti);
 extern void vacuum_set_xid_limits(int freeze_min_age, int freeze_table_age,
+					  int multixact_freeze_min_age,
+					  int multixact_freeze_table_age,
 					  bool sharedRel,
 					  TransactionId *oldestXmin,
 					  TransactionId *freezeLimit,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0eac9fb..d749f70 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2430,6 +2430,8 @@ typedef struct VacuumStmt
 	int			options;		/* OR of VacuumOption flags */
 	int			freeze_min_age; /* min freeze age, or -1 to use default */
 	int			freeze_table_age;		/* age at which to scan whole table */
+	int			multixact_freeze_min_age; /* min multixact freeze age, or -1 to use default */
+	int			multixact_freeze_table_age; /* multixact age at which to scan whole table */
 	RangeVar   *relation;		/* single table to process, or NULL */
 	List	   *va_cols;		/* list of column names, or NIL for all */
 } VacuumStmt;
diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h
index e96f07a..70e0566 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -24,6 +24,7 @@ extern double autovacuum_vac_scale;
 extern int	autovacuum_anl_thresh;
 extern double autovacuum_anl_scale;
 extern int	autovacuum_freeze_max_age;
+extern int	autovacuum_multixact_freeze_max_age;
 extern int	autovacuum_vac_cost_delay;
 extern int	autovacuum_vac_cost_limit;
 
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 58cc3f7..47ae106 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -198,6 +198,9 @@ typedef struct AutoVacOpts
 	int			freeze_min_age;
 	int			freeze_max_age;
 	int			freeze_table_age;
+	int			multixact_freeze_min_age;
+	int			multixact_freeze_max_age;
+	int			multixact_freeze_table_age;
 	float8		vacuum_scale_factor;
 	float8		analyze_scale_factor;
 } AutoVacOpts;
#21Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#20)
Re: truncating pg_multixact/members

On Tue, Feb 11, 2014 at 5:16 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Robert Haas escribió:

On Mon, Jan 20, 2014 at 1:39 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

* I haven't introduced settings to tweak this per table for
autovacuum. I don't think those are needed. It's not hard to do,
however; if people opine against this, I will implement that.

I can't think of any reason to believe that it will be less important
to tune these values on a per-table basis than it is to be able to do
the same with the autovacuum parameters. Indeed, all the discussion
on this thread suggests precisely that we have no real idea how to set
these values yet, so more configurability is good. Even if you reject
that argument, I think it's a bad idea to start making xmax vacuuming
and xmin vacuuming less than parallel; such decisions confuse users.

Yeah, I can relate to this argument. I have added per-table
configurability to this, and also added the an equivalent of
autovacuum_freeze_max_age to force a for-wraparound full scan of a table
based on multixacts.

I haven't really tested this beyond ensuring that it compiles, and I
haven't changed the default values, but here it is in case someone wants
to have a look and comment --- particularly on the doc additions.

Using Multixact capitalized just so seems odd to me. Probably should
be lower case (multiple places). This part needs some copy-editing:

+       <para>
+        Vacuum also allows removal of old files from the
+        <filename>pg_multixact/members</> and <filename>pg_multixact/offsets</>
+        subdirectories, which is why the default is a relatively low
+        50 million transactions.

Vacuuming multixacts also allows...? And: 50 million multixacts, not
transactions.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#21)
1 attachment(s)
Re: truncating pg_multixact/members

In this new version, I added a couple of fields to VacuumStmt node. How
strongly do we feel this would cause an ABI break? Would we be more
comfortable if I put them at the end of the struct for 9.3 instead?
Do we expect third-party code to be calling vacuum()?

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

Here is patch v3, which should be final or close to. Changes from
previous:

Robert Haas wrote:

Using Multixact capitalized just so seems odd to me. Probably should
be lower case (multiple places).

Changed it to be all lower case. Originally the X was also upper case,
which looked even odder.

This part needs some copy-editing:

+       <para>
+        Vacuum also allows removal of old files from the
+        <filename>pg_multixact/members</> and <filename>pg_multixact/offsets</>
+        subdirectories, which is why the default is a relatively low
+        50 million transactions.

Vacuuming multixacts also allows...? And: 50 million multixacts, not
transactions.

I reworded this rather completely.

I was missing a change to SetMultiXactIdLimit to use the multixact value
instead of the one for Xids, and passing the values computed by
autovacuum to vacuum().

Per discussion, new default values are 150 million for
vacuum_multixact_freeze_table_age (same as the one for Xids), and 5
million for vacuum_multixact_freeze_min_age. I decided to raise
autovacuum_multixact_freeze_max_age to 400 million, i.e. double the one
for Xids; so there should be no more emergency vacuuming than before
unless multixact consumption is more than double that for Xids. (Now
that I re-read this, the same rationale would have me setting the
default for vacuum_multixact_freeze_table_age to 300 million. Any votes
on that?).

I adjusted the default values everywhere (docs and sample config), and
fixed one or two typos in the docco for Xid vacuuming that I happened to
notice, as well. postgresql.conf.sample contained a couple of
space-before-tab which I removed.

<!-- I thought about using a struct to pass all four values around in
multiple routines rather than 4 ints (vacuum_set_xid_limits,
cluster_rel, rebuild_relation, copy_heap_data). Decided not to for the
time being. Perhaps a patch for HEAD only. -->

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

mxact-freeze-age-3.patchtext/x-diff; charset=us-asciiDownload
*** a/doc/src/sgml/config.sgml
--- b/doc/src/sgml/config.sgml
***************
*** 4730,4735 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 4730,4762 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
+       <term><varname>autovacuum_multixact_freeze_max_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>autovacuum_multixact_freeze_max_age</varname> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         Specifies the maximum age (in multixacts) that a table's
+         <structname>pg_class</>.<structfield>relminmxid</> field can
+         attain before a <command>VACUUM</> operation is forced to
+         prevent multixact ID wraparound within the table.
+         Note that the system will launch autovacuum processes to
+         prevent wraparound even when autovacuum is otherwise disabled.
+        </para>
+ 
+        <para>
+         Vacuuming multixacts also allows removal of old files from the
+         <filename>pg_multixact/members</> and <filename>pg_multixact/offsets</>
+         subdirectories, which is why the default is a relatively low
+         400 million multixacts.
+         This parameter can only be set at server start, but the setting
+         can be reduced for individual tables by changing storage parameters.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
        <term><varname>autovacuum_vacuum_cost_delay</varname> (<type>integer</type>)</term>
        <indexterm>
***************
*** 5138,5144 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
          <structname>pg_class</>.<structfield>relfrozenxid</> field has reached
          the age specified by this setting.  The default is 150 million
          transactions.  Although users can set this value anywhere from zero to
!         one billion, <command>VACUUM</> will silently limit the effective value
          to 95% of <xref linkend="guc-autovacuum-freeze-max-age">, so that a
          periodical manual <command>VACUUM</> has a chance to run before an
          anti-wraparound autovacuum is launched for the table. For more
--- 5165,5171 ----
          <structname>pg_class</>.<structfield>relfrozenxid</> field has reached
          the age specified by this setting.  The default is 150 million
          transactions.  Although users can set this value anywhere from zero to
!         two billions, <command>VACUUM</> will silently limit the effective value
          to 95% of <xref linkend="guc-autovacuum-freeze-max-age">, so that a
          periodical manual <command>VACUUM</> has a chance to run before an
          anti-wraparound autovacuum is launched for the table. For more
***************
*** 5169,5174 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 5196,5242 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-vacuum-multixact-freeze-table-age" xreflabel="vacuum_multixact_freeze_table_age">
+       <term><varname>vacuum_multixact_freeze_table_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>vacuum_multixact_freeze_table_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         <command>VACUUM</> performs a whole-table scan if the table's
+         <structname>pg_class</>.<structfield>relminmxid</> field has reached
+         the age specified by this setting.  The default is 150 million multixacts.
+         Although users can set this value anywhere from zero to two billions,
+         <command>VACUUM</> will silently limit the effective value to 95% of
+         <xref linkend="guc-autovacuum-multixact-freeze-max-age">, so that a
+         periodical manual <command>VACUUM</> has a chance to run before an
+         anti-wraparound is launched for the table.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
+      <varlistentry id="guc-vacuum-multixact-freeze-min-age" xreflabel="vacuum_multixact_freeze_min_age">
+       <term><varname>vacuum_multixact_freeze_min_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>vacuum_multixact_freeze_min_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         Specifies the cutoff age (in multixacts) that <command>VACUUM</>
+         should use to decide whether to replace multixact IDs with a newer
+         transaction ID or multixact ID while scanning a table.  The default
+         is 5 million multixacts.
+         Although users can set this value anywhere from zero to one billion,
+         <command>VACUUM</> will silently limit the effective value to half
+         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age">,
+         so that there is not an unreasonably short time between forced
+         autovacuums.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-bytea-output" xreflabel="bytea_output">
        <term><varname>bytea_output</varname> (<type>enum</type>)</term>
        <indexterm>
*** a/doc/src/sgml/maintenance.sgml
--- b/doc/src/sgml/maintenance.sgml
***************
*** 108,114 ****
  
       <listitem>
        <simpara>To protect against loss of very old data due to
!       <firstterm>transaction ID wraparound</>.</simpara>
       </listitem>
      </orderedlist>
  
--- 108,115 ----
  
       <listitem>
        <simpara>To protect against loss of very old data due to
!       <firstterm>transaction ID wraparound</> or
!       <firstterm>multixact ID wraparound</>.</simpara>
       </listitem>
      </orderedlist>
  
***************
*** 379,384 ****
--- 380,390 ----
      <secondary>wraparound</secondary>
     </indexterm>
  
+     <indexterm>
+      <primary>wraparound</primary>
+      <secondary>of transaction IDs</secondary>
+     </indexterm>
+ 
     <para>
      <productname>PostgreSQL</productname>'s MVCC transaction semantics
      depend on being able to compare transaction ID (<acronym>XID</>)
***************
*** 599,604 **** HINT:  Stop the postmaster and use a standalone backend to VACUUM in "mydb".
--- 605,658 ----
      page for details about using a single-user backend.
     </para>
  
+    <sect3 id="vacuum-for-multixact-wraparound">
+     <title>Multixacts and Wraparound</title>
+ 
+     <indexterm>
+      <primary>MultiXactId</primary>
+     </indexterm>
+ 
+     <indexterm>
+      <primary>wraparound</primary>
+      <secondary>of multixact IDs</secondary>
+     </indexterm>
+ 
+     <para>
+      <firstterm>Multixacts</> are used to implement row locking by
+      multiple transactions: since there is limited space in the tuple
+      header to store lock information, that information is stored as a
+      multixact separately in the <filename>pg_multixact</> subdirectory,
+      and only its ID is in the <structfield>xmax</> field
+      in the tuple header.
+      Similar to transaction IDs, multixact IDs are implemented as a
+      32-bit counter and corresponding storage, all of which requires
+      careful aging management, storage cleanup, and wraparound handling.
+     </para>
+ 
+     <para>
+      During a <command>VACUUM</> table scan, either partial or of the whole
+      table, any multixact ID older than
+      <xref linkend="guc-vacuum-multixact-freeze-min-age">
+      is replaced by a different value, which can be the zero value, a single
+      transaction ID, or a newer multixact ID.  For each table,
+      <structname>pg_class</>.<structfield>relminmxid</> stores the oldest
+      possible value still stored in any tuple of that table.  Every time this
+      value is older than
+      <xref linkend="guc-vacuum-multixact-freeze-table-age">, a whole-table
+      scan is forced.  Whole-table <command>VACUUM</> scans, regardless of
+      what causes them, enable advancing the value for that table.
+      Eventually, as all tables in all databases are scanned and their
+      oldest multixact values are advanced, on-disk storage for older
+      multixacts can be removed.
+     </para>
+ 
+     <para>
+      As a safety device, a whole-table vacuum scan will occur for any table
+      whose multixact-age is greater than
+      <xref linkend="guc-autovacuum-multixact-freeze-max-age">.
+      This will occur even if autovacuum is nominally disabled.
+     </para>
+    </sect3>
    </sect2>
  
    <sect2 id="autovacuum">
*** a/doc/src/sgml/ref/create_table.sgml
--- b/doc/src/sgml/ref/create_table.sgml
***************
*** 981,987 **** CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
       <para>
       Custom <xref linkend="guc-vacuum-freeze-min-age"> parameter. Note that
       autovacuum will ignore attempts to set a per-table
!      <literal>autovacuum_freeze_min_age</> larger than the half system-wide
       <xref linkend="guc-autovacuum-freeze-max-age"> setting.
       </para>
      </listitem>
--- 981,987 ----
       <para>
       Custom <xref linkend="guc-vacuum-freeze-min-age"> parameter. Note that
       autovacuum will ignore attempts to set a per-table
!      <literal>autovacuum_freeze_min_age</> larger than half the system-wide
       <xref linkend="guc-autovacuum-freeze-max-age"> setting.
       </para>
      </listitem>
***************
*** 1010,1015 **** CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
--- 1010,1052 ----
      </listitem>
     </varlistentry>
  
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-vacuum-multixact-freeze-min-age"> parameter.
+       Note that autovacuum will ignore attempts to set a per-table
+       <literal>autovacuum_multixact_freeze_min_age</> larger than half the
+       system-wide <xref linkend="guc-autovacuum-multixact-freeze-max-age">
+       setting.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-autovacuum-multixact-freeze-max-age"> parameter. Note
+       that autovacuum will ignore attempts to set a per-table
+       <literal>autovacuum_multixact_freeze_max_age</> larger than the
+       system-wide setting (it can only be set smaller).  Note that while you
+       can set <literal>autovacuum_multixact_freeze_max_age</> very small,
+       or even zero, this is usually unwise since it will force frequent
+       vacuuming.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-vacuum-multixact-freeze-table-age"> parameter.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
     </variablelist>
  
    </refsect2>
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 164,169 **** static relopt_int intRelOpts[] =
--- 164,177 ----
  	},
  	{
  		{
+ 			"autovacuum_multixact_freeze_min_age",
+ 			"Minimum MultiXact age at which VACUUM should freeze a row MultiXact's, for autovacuum",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		},
+ 		-1, 0, 1000000000
+ 	},
+ 	{
+ 		{
  			"autovacuum_freeze_max_age",
  			"Age at which to autovacuum a table to prevent transaction ID wraparound",
  			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
***************
*** 172,182 **** static relopt_int intRelOpts[] =
--- 180,205 ----
  	},
  	{
  		{
+ 			"autovacuum_multixact_freeze_max_age",
+ 			"MultiXact age at which to autovacuum a table to prevent MultiXact wraparound",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		},
+ 		-1, 100000000, 2000000000
+ 	},
+ 	{
+ 		{
  			"autovacuum_freeze_table_age",
  			"Age at which VACUUM should perform a full table sweep to replace old Xid values with FrozenXID",
  			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
  		}, -1, 0, 2000000000
  	},
+ 	{
+ 		{
+ 			"autovacuum_multixact_freeze_table_age",
+ 			"Age of MultiXact at which VACUUM should perform a full table sweep to replace old MultiXact values with newer ones",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		}, -1, 0, 2000000000
+ 	},
  	/* list terminator */
  	{{NULL}}
  };
*** a/src/backend/access/transam/multixact.c
--- b/src/backend/access/transam/multixact.c
***************
*** 2055,2065 **** SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid)
  	Assert(MultiXactIdIsValid(oldest_datminmxid));
  
  	/*
! 	 * The place where we actually get into deep trouble is halfway around
! 	 * from the oldest potentially-existing XID/multi.	(This calculation is
! 	 * probably off by one or two counts for Xids, because the special XIDs
! 	 * reduce the size of the loop a little bit.  But we throw in plenty of
! 	 * slop below, so it doesn't matter.)
  	 */
  	multiWrapLimit = oldest_datminmxid + (MaxMultiXactId >> 1);
  	if (multiWrapLimit < FirstMultiXactId)
--- 2055,2067 ----
  	Assert(MultiXactIdIsValid(oldest_datminmxid));
  
  	/*
! 	 * Since multixacts wrap differently from transaction IDs, this logic is
! 	 * not entirely correct: in some scenarios we could go for longer than 2
! 	 * billion multixacts without seeing any data loss, and in some others we
! 	 * could get in trouble before that if the new pg_multixact/members data
! 	 * stomps on the previous cycle's data.  For lack of a better mechanism we
! 	 * use the same logic as for transaction IDs, that is, start taking action
! 	 * halfway around the oldest potentially-existing multixact.
  	 */
  	multiWrapLimit = oldest_datminmxid + (MaxMultiXactId >> 1);
  	if (multiWrapLimit < FirstMultiXactId)
***************
*** 2093,2104 **** SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid)
  
  	/*
  	 * We'll start trying to force autovacuums when oldest_datminmxid gets to
! 	 * be more than autovacuum_freeze_max_age mxids old.
  	 *
! 	 * It's a bit ugly to just reuse limits for xids that way, but it doesn't
! 	 * seem worth adding separate GUCs for that purpose.
  	 */
! 	multiVacLimit = oldest_datminmxid + autovacuum_freeze_max_age;
  	if (multiVacLimit < FirstMultiXactId)
  		multiVacLimit += FirstMultiXactId;
  
--- 2095,2107 ----
  
  	/*
  	 * We'll start trying to force autovacuums when oldest_datminmxid gets to
! 	 * be more than autovacuum_multixact_freeze_max_age mxids old.
  	 *
! 	 * Note: autovacuum_multixact_freeze_max_age is a PGC_POSTMASTER parameter
! 	 * so that we don't have to worry about dealing with on-the-fly changes
! 	 * in its value.  See SetTransactionIdLimit.
  	 */
! 	multiVacLimit = oldest_datminmxid + autovacuum_multixact_freeze_max_age;
  	if (multiVacLimit < FirstMultiXactId)
  		multiVacLimit += FirstMultiXactId;
  
*** a/src/backend/access/transam/varsup.c
--- b/src/backend/access/transam/varsup.c
***************
*** 313,319 **** SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid)
  	 * value.  It doesn't look practical to update shared state from a GUC
  	 * assign hook (too many processes would try to execute the hook,
  	 * resulting in race conditions as well as crashes of those not connected
! 	 * to shared memory).  Perhaps this can be improved someday.
  	 */
  	xidVacLimit = oldest_datfrozenxid + autovacuum_freeze_max_age;
  	if (xidVacLimit < FirstNormalTransactionId)
--- 313,320 ----
  	 * value.  It doesn't look practical to update shared state from a GUC
  	 * assign hook (too many processes would try to execute the hook,
  	 * resulting in race conditions as well as crashes of those not connected
! 	 * to shared memory).  Perhaps this can be improved someday.  See also
! 	 * SetMultiXactIdLimit.
  	 */
  	xidVacLimit = oldest_datfrozenxid + autovacuum_freeze_max_age;
  	if (xidVacLimit < FirstNormalTransactionId)
*** a/src/backend/commands/cluster.c
--- b/src/backend/commands/cluster.c
***************
*** 64,72 **** typedef struct
  
  
  static void rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age, bool verbose);
  static void copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age, bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti);
  static List *get_tables_to_cluster(MemoryContext cluster_context);
--- 64,76 ----
  
  
  static void rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age,
! 				 int multixact_freeze_min_age, int multixact_freeze_table_age,
! 				 bool verbose);
  static void copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age,
! 			   int multixact_freeze_min_age, int multixact_freeze_table_age,
! 			   bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti);
  static List *get_tables_to_cluster(MemoryContext cluster_context);
***************
*** 179,185 **** cluster(ClusterStmt *stmt, bool isTopLevel)
  		 * Do the job.  We use a -1 freeze_min_age to avoid having CLUSTER
  		 * freeze tuples earlier than a plain VACUUM would.
  		 */
! 		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1);
  	}
  	else
  	{
--- 183,189 ----
  		 * Do the job.  We use a -1 freeze_min_age to avoid having CLUSTER
  		 * freeze tuples earlier than a plain VACUUM would.
  		 */
! 		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1, -1, -1);
  	}
  	else
  	{
***************
*** 230,236 **** cluster(ClusterStmt *stmt, bool isTopLevel)
  			PushActiveSnapshot(GetTransactionSnapshot());
  			/* Do the job.  As above, use a -1 freeze_min_age. */
  			cluster_rel(rvtc->tableOid, rvtc->indexOid, true, stmt->verbose,
! 						-1, -1);
  			PopActiveSnapshot();
  			CommitTransactionCommand();
  		}
--- 234,240 ----
  			PushActiveSnapshot(GetTransactionSnapshot());
  			/* Do the job.  As above, use a -1 freeze_min_age. */
  			cluster_rel(rvtc->tableOid, rvtc->indexOid, true, stmt->verbose,
! 						-1, -1, -1, -1);
  			PopActiveSnapshot();
  			CommitTransactionCommand();
  		}
***************
*** 262,268 **** cluster(ClusterStmt *stmt, bool isTopLevel)
   */
  void
  cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
! 			int freeze_min_age, int freeze_table_age)
  {
  	Relation	OldHeap;
  
--- 266,273 ----
   */
  void
  cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
! 			int freeze_min_age, int freeze_table_age,
! 			int multixact_freeze_min_age, int multixact_freeze_table_age)
  {
  	Relation	OldHeap;
  
***************
*** 407,412 **** cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
--- 412,418 ----
  
  	/* rebuild_relation does all the dirty work */
  	rebuild_relation(OldHeap, indexOid, freeze_min_age, freeze_table_age,
+ 					 multixact_freeze_min_age, multixact_freeze_table_age,
  					 verbose);
  
  	/* NB: rebuild_relation does heap_close() on OldHeap */
***************
*** 566,572 **** mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
   */
  static void
  rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age, bool verbose)
  {
  	Oid			tableOid = RelationGetRelid(OldHeap);
  	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
--- 572,580 ----
   */
  static void
  rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age,
! 				 int multixact_freeze_min_age, int multixact_freeze_table_age,
! 				 bool verbose)
  {
  	Oid			tableOid = RelationGetRelid(OldHeap);
  	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
***************
*** 591,597 **** rebuild_relation(Relation OldHeap, Oid indexOid,
  
  	/* Copy the heap data into the new table in the desired order */
  	copy_heap_data(OIDNewHeap, tableOid, indexOid,
! 				   freeze_min_age, freeze_table_age, verbose,
  				   &swap_toast_by_content, &frozenXid, &cutoffMulti);
  
  	/*
--- 599,607 ----
  
  	/* Copy the heap data into the new table in the desired order */
  	copy_heap_data(OIDNewHeap, tableOid, indexOid,
! 				   freeze_min_age, freeze_table_age,
! 				   multixact_freeze_min_age, multixact_freeze_table_age,
! 				   verbose,
  				   &swap_toast_by_content, &frozenXid, &cutoffMulti);
  
  	/*
***************
*** 733,739 **** make_new_heap(Oid OIDOldHeap, Oid NewTableSpace)
   */
  static void
  copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age, bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti)
  {
--- 743,751 ----
   */
  static void
  copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age,
! 			   int multixact_freeze_min_age, int multixact_freeze_table_age,
! 			   bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti)
  {
***************
*** 849,854 **** copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
--- 861,867 ----
  	 * compute xids used to freeze and weed out dead tuples.
  	 */
  	vacuum_set_xid_limits(freeze_min_age, freeze_table_age,
+ 						  multixact_freeze_min_age, multixact_freeze_table_age,
  						  OldHeap->rd_rel->relisshared,
  						  &OldestXmin, &FreezeXid, NULL, &MultiXactCutoff,
  						  NULL);
*** a/src/backend/commands/vacuum.c
--- b/src/backend/commands/vacuum.c
***************
*** 55,60 ****
--- 55,62 ----
   */
  int			vacuum_freeze_min_age;
  int			vacuum_freeze_table_age;
+ int			vacuum_multixact_freeze_min_age;
+ int			vacuum_multixact_freeze_table_age;
  
  
  /* A few variables that don't seem worth passing around as parameters */
***************
*** 398,403 **** get_rel_oids(Oid relid, const RangeVar *vacrel)
--- 400,407 ----
  void
  vacuum_set_xid_limits(int freeze_min_age,
  					  int freeze_table_age,
+ 					  int multixact_freeze_min_age,
+ 					  int multixact_freeze_table_age,
  					  bool sharedRel,
  					  TransactionId *oldestXmin,
  					  TransactionId *freezeLimit,
***************
*** 406,414 **** vacuum_set_xid_limits(int freeze_min_age,
--- 410,420 ----
  					  MultiXactId *mxactFullScanLimit)
  {
  	int			freezemin;
+ 	int			mxid_freezemin;
  	TransactionId limit;
  	TransactionId safeLimit;
  	MultiXactId	mxactLimit;
+ 	MultiXactId	safeMxactLimit;
  
  	/*
  	 * We can always ignore processes running lazy vacuum.	This is because we
***************
*** 462,474 **** vacuum_set_xid_limits(int freeze_min_age,
  	*freezeLimit = limit;
  
  	/*
! 	 * simplistic MultiXactId removal limit: use the same policy as for
! 	 * freezing Xids (except we use the oldest known mxact instead of the
! 	 * current next value).
  	 */
! 	mxactLimit = GetOldestMultiXactId() - freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
  	*multiXactCutoff = mxactLimit;
  
  	if (xidFullScanLimit != NULL)
--- 468,503 ----
  	*freezeLimit = limit;
  
  	/*
! 	 * Determine the minimum multixact freeze age to use: as specified by
! 	 * caller, or vacuum_multixact_freeze_min_age, but in any case not more
! 	 * than half autovacuum_multixact_freeze_max_age, so that autovacuums to
! 	 * prevent MultiXact wraparound won't occur too frequently.
  	 */
! 	mxid_freezemin = multixact_freeze_min_age;
! 	if (mxid_freezemin < 0)
! 		mxid_freezemin = vacuum_multixact_freeze_min_age;
! 	mxid_freezemin = Min(mxid_freezemin,
! 						 autovacuum_multixact_freeze_max_age / 2);
! 	Assert(mxid_freezemin >= 0);
! 
! 	/* compute the cutoff multi, being careful to generate a valid value */
! 	mxactLimit = GetOldestMultiXactId() - mxid_freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
+ 
+ 	safeMxactLimit =
+ 		ReadNextMultiXactId() - autovacuum_multixact_freeze_max_age;
+ 	if (safeMxactLimit < FirstMultiXactId)
+ 		safeMxactLimit = FirstMultiXactId;
+ 
+ 	if (MultiXactIdPrecedes(mxactLimit, safeMxactLimit))
+ 	{
+ 		ereport(WARNING,
+ 				(errmsg("oldest MultiXact is far in the past"),
+ 				 errhint("Close open transactions with MultiXacts soon to avoid wraparound problems.")));
+ 		mxactLimit = safeMxactLimit;
+ 	}
+ 
  	*multiXactCutoff = mxactLimit;
  
  	if (xidFullScanLimit != NULL)
***************
*** 503,509 **** vacuum_set_xid_limits(int freeze_min_age,
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * (and limits) from plain XIDs here.
  		 */
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
--- 532,548 ----
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * from plain XIDs here.
! 		 */
! 		freezetable = multixact_freeze_table_age;
! 		if (freezetable < 0)
! 			freezetable = vacuum_multixact_freeze_table_age;
! 		freezetable = Min(freezetable,
! 						  autovacuum_multixact_freeze_max_age * 0.95);
! 		Assert(freezetable >= 0);
! 
! 		/* Compute MultiXact limit causing a full-table vacuum, being careful
! 		 * to generate a valid MultiXact value.
  		 */
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
***************
*** 511,516 **** vacuum_set_xid_limits(int freeze_min_age,
--- 550,559 ----
  
  		*mxactFullScanLimit = mxactLimit;
  	}
+ 	else
+ 	{
+ 		Assert(mxactFullScanLimit == NULL);
+ 	}
  }
  
  /*
***************
*** 1150,1156 **** vacuum_rel(Oid relid, VacuumStmt *vacstmt, bool do_toast, bool for_wraparound)
  		/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
  		cluster_rel(relid, InvalidOid, false,
  					(vacstmt->options & VACOPT_VERBOSE) != 0,
! 					vacstmt->freeze_min_age, vacstmt->freeze_table_age);
  	}
  	else
  		lazy_vacuum_rel(onerel, vacstmt, vac_strategy);
--- 1193,1201 ----
  		/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
  		cluster_rel(relid, InvalidOid, false,
  					(vacstmt->options & VACOPT_VERBOSE) != 0,
! 					vacstmt->freeze_min_age, vacstmt->freeze_table_age,
! 					vacstmt->multixact_freeze_min_age,
! 					vacstmt->multixact_freeze_table_age);
  	}
  	else
  		lazy_vacuum_rel(onerel, vacstmt, vac_strategy);
*** a/src/backend/commands/vacuumlazy.c
--- b/src/backend/commands/vacuumlazy.c
***************
*** 203,208 **** lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt,
--- 203,210 ----
  	vac_strategy = bstrategy;
  
  	vacuum_set_xid_limits(vacstmt->freeze_min_age, vacstmt->freeze_table_age,
+ 						  vacstmt->multixact_freeze_min_age,
+ 						  vacstmt->multixact_freeze_table_age,
  						  onerel->rd_rel->relisshared,
  						  &OldestXmin, &FreezeLimit, &xidFullScanLimit,
  						  &MultiXactCutoff, &mxactFullScanLimit);
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 3206,3211 **** _copyVacuumStmt(const VacuumStmt *from)
--- 3206,3213 ----
  	COPY_SCALAR_FIELD(options);
  	COPY_SCALAR_FIELD(freeze_min_age);
  	COPY_SCALAR_FIELD(freeze_table_age);
+ 	COPY_SCALAR_FIELD(multixact_freeze_min_age);
+ 	COPY_SCALAR_FIELD(multixact_freeze_table_age);
  	COPY_NODE_FIELD(relation);
  	COPY_NODE_FIELD(va_cols);
  
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 1496,1501 **** _equalVacuumStmt(const VacuumStmt *a, const VacuumStmt *b)
--- 1496,1503 ----
  	COMPARE_SCALAR_FIELD(options);
  	COMPARE_SCALAR_FIELD(freeze_min_age);
  	COMPARE_SCALAR_FIELD(freeze_table_age);
+ 	COMPARE_SCALAR_FIELD(multixact_freeze_min_age);
+ 	COMPARE_SCALAR_FIELD(multixact_freeze_table_age);
  	COMPARE_NODE_FIELD(relation);
  	COMPARE_NODE_FIELD(va_cols);
  
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 8474,8479 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8474,8481 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8488,8493 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8490,8497 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					n->relation = $5;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8502,8507 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8506,8513 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					$$ = (Node *)n;
  				}
  			| VACUUM '(' vacuum_option_list ')'
***************
*** 8509,8517 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8515,8531 ----
  					VacuumStmt *n = makeNode(VacuumStmt);
  					n->options = VACOPT_VACUUM | $3;
  					if (n->options & VACOPT_FREEZE)
+ 					{
  						n->freeze_min_age = n->freeze_table_age = 0;
+ 						n->multixact_freeze_min_age = 0;
+ 						n->multixact_freeze_table_age = 0;
+ 					}
  					else
+ 					{
  						n->freeze_min_age = n->freeze_table_age = -1;
+ 						n->multixact_freeze_min_age = -1;
+ 						n->multixact_freeze_table_age = -1;
+ 					}
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *) n;
***************
*** 8521,8529 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8535,8551 ----
  					VacuumStmt *n = makeNode(VacuumStmt);
  					n->options = VACOPT_VACUUM | $3;
  					if (n->options & VACOPT_FREEZE)
+ 					{
  						n->freeze_min_age = n->freeze_table_age = 0;
+ 						n->multixact_freeze_min_age = 0;
+ 						n->multixact_freeze_table_age = 0;
+ 					}
  					else
+ 					{
  						n->freeze_min_age = n->freeze_table_age = -1;
+ 						n->multixact_freeze_min_age = -1;
+ 						n->multixact_freeze_table_age = -1;
+ 					}
  					n->relation = $5;
  					n->va_cols = $6;
  					if (n->va_cols != NIL)	/* implies analyze */
***************
*** 8553,8558 **** AnalyzeStmt:
--- 8575,8582 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = -1;
  					n->freeze_table_age = -1;
+ 					n->multixact_freeze_min_age = -1;
+ 					n->multixact_freeze_table_age = -1;
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8565,8570 **** AnalyzeStmt:
--- 8589,8596 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = -1;
  					n->freeze_table_age = -1;
+ 					n->multixact_freeze_min_age = -1;
+ 					n->multixact_freeze_table_age = -1;
  					n->relation = $3;
  					n->va_cols = $4;
  					$$ = (Node *)n;
*** a/src/backend/postmaster/autovacuum.c
--- b/src/backend/postmaster/autovacuum.c
***************
*** 116,121 **** double		autovacuum_vac_scale;
--- 116,122 ----
  int			autovacuum_anl_thresh;
  double		autovacuum_anl_scale;
  int			autovacuum_freeze_max_age;
+ int			autovacuum_multixact_freeze_max_age;
  
  int			autovacuum_vac_cost_delay;
  int			autovacuum_vac_cost_limit;
***************
*** 144,149 **** static MultiXactId recentMulti;
--- 145,152 ----
  /* Default freeze ages to use for autovacuum (varies by database) */
  static int	default_freeze_min_age;
  static int	default_freeze_table_age;
+ static int	default_multixact_freeze_min_age;
+ static int	default_multixact_freeze_table_age;
  
  /* Memory context for long-lived data */
  static MemoryContext AutovacMemCxt;
***************
*** 185,190 **** typedef struct autovac_table
--- 188,195 ----
  	bool		at_doanalyze;
  	int			at_freeze_min_age;
  	int			at_freeze_table_age;
+ 	int			at_multixact_freeze_min_age;
+ 	int			at_multixact_freeze_table_age;
  	int			at_vacuum_cost_delay;
  	int			at_vacuum_cost_limit;
  	bool		at_wraparound;
***************
*** 1129,1135 **** do_start_worker(void)
  
  	/* Also determine the oldest datminmxid we will consider. */
  	recentMulti = ReadNextMultiXactId();
! 	multiForceLimit = recentMulti - autovacuum_freeze_max_age;
  	if (multiForceLimit < FirstMultiXactId)
  		multiForceLimit -= FirstMultiXactId;
  
--- 1134,1140 ----
  
  	/* Also determine the oldest datminmxid we will consider. */
  	recentMulti = ReadNextMultiXactId();
! 	multiForceLimit = recentMulti - autovacuum_multixact_freeze_max_age;
  	if (multiForceLimit < FirstMultiXactId)
  		multiForceLimit -= FirstMultiXactId;
  
***************
*** 1955,1965 **** do_autovacuum(void)
--- 1960,1974 ----
  	{
  		default_freeze_min_age = 0;
  		default_freeze_table_age = 0;
+ 		default_multixact_freeze_min_age = 0;
+ 		default_multixact_freeze_table_age = 0;
  	}
  	else
  	{
  		default_freeze_min_age = vacuum_freeze_min_age;
  		default_freeze_table_age = vacuum_freeze_table_age;
+ 		default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age;
+ 		default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
  	}
  
  	ReleaseSysCache(tuple);
***************
*** 2510,2515 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2519,2526 ----
  	{
  		int			freeze_min_age;
  		int			freeze_table_age;
+ 		int			multixact_freeze_min_age;
+ 		int			multixact_freeze_table_age;
  		int			vac_cost_limit;
  		int			vac_cost_delay;
  
***************
*** 2543,2554 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2554,2577 ----
  			? avopts->freeze_table_age
  			: default_freeze_table_age;
  
+ 		multixact_freeze_min_age = (avopts &&
+ 									avopts->multixact_freeze_min_age >= 0)
+ 			? avopts->multixact_freeze_min_age
+ 			: default_multixact_freeze_min_age;
+ 
+ 		multixact_freeze_table_age = (avopts &&
+ 									  avopts->multixact_freeze_table_age >= 0)
+ 			? avopts->multixact_freeze_table_age
+ 			: default_multixact_freeze_table_age;
+ 
  		tab = palloc(sizeof(autovac_table));
  		tab->at_relid = relid;
  		tab->at_dovacuum = dovacuum;
  		tab->at_doanalyze = doanalyze;
  		tab->at_freeze_min_age = freeze_min_age;
  		tab->at_freeze_table_age = freeze_table_age;
+ 		tab->at_multixact_freeze_min_age = multixact_freeze_min_age;
+ 		tab->at_multixact_freeze_table_age = multixact_freeze_table_age;
  		tab->at_vacuum_cost_limit = vac_cost_limit;
  		tab->at_vacuum_cost_delay = vac_cost_delay;
  		tab->at_wraparound = wraparound;
***************
*** 2754,2759 **** autovacuum_do_vac_analyze(autovac_table *tab,
--- 2777,2784 ----
  		vacstmt.options |= VACOPT_ANALYZE;
  	vacstmt.freeze_min_age = tab->at_freeze_min_age;
  	vacstmt.freeze_table_age = tab->at_freeze_table_age;
+ 	vacstmt.multixact_freeze_min_age = tab->at_multixact_freeze_min_age;
+ 	vacstmt.multixact_freeze_table_age = tab->at_multixact_freeze_table_age;
  	/* we pass the OID, but might need this anyway for an error message */
  	vacstmt.relation = &rangevar;
  	vacstmt.va_cols = NIL;
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
***************
*** 1907,1912 **** static struct config_int ConfigureNamesInt[] =
--- 1907,1932 ----
  	},
  
  	{
+ 		{"vacuum_multixact_freeze_min_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Minimum age at which VACUUM should freeze a MultiXactId in a table row."),
+ 			NULL
+ 		},
+ 		&vacuum_multixact_freeze_min_age,
+ 		5000000, 0, 1000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
+ 		{"vacuum_multixact_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."),
+ 			NULL
+ 		},
+ 		&vacuum_multixact_freeze_table_age,
+ 		150000000, 0, 2000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
  		{"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_MASTER,
  			gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."),
  			NULL
***************
*** 2296,2301 **** static struct config_int ConfigureNamesInt[] =
--- 2316,2331 ----
  		NULL, NULL, NULL
  	},
  	{
+ 		/* see varsup.c for why this is PGC_POSTMASTER not PGC_SIGHUP */
+ 		{"autovacuum_multixact_freeze_max_age", PGC_POSTMASTER, AUTOVACUUM,
+ 			gettext_noop("Multixact age at which to autovacuum a table to prevent multixact wraparound."),
+ 			NULL
+ 		},
+ 		&autovacuum_multixact_freeze_max_age,
+ 		400000000, 10000000, 2000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 	{
  		/* see max_connections */
  		{"autovacuum_max_workers", PGC_POSTMASTER, AUTOVACUUM,
  			gettext_noop("Sets the maximum number of simultaneously running autovacuum worker processes."),
*** a/src/backend/utils/misc/postgresql.conf.sample
--- b/src/backend/utils/misc/postgresql.conf.sample
***************
*** 362,373 ****
  					#   panic
  
  #log_min_error_statement = error	# values in order of decreasing detail:
! 				 	#   debug5
  					#   debug4
  					#   debug3
  					#   debug2
  					#   debug1
! 				 	#   info
  					#   notice
  					#   warning
  					#   error
--- 362,373 ----
  					#   panic
  
  #log_min_error_statement = error	# values in order of decreasing detail:
! 					#   debug5
  					#   debug4
  					#   debug3
  					#   debug2
  					#   debug1
! 					#   info
  					#   notice
  					#   warning
  					#   error
***************
*** 431,437 ****
  #track_counts = on
  #track_io_timing = off
  #track_functions = none			# none, pl, all
! #track_activity_query_size = 1024 	# (change requires restart)
  #update_process_title = on
  #stats_temp_directory = 'pg_stat_tmp'
  
--- 431,437 ----
  #track_counts = on
  #track_io_timing = off
  #track_functions = none			# none, pl, all
! #track_activity_query_size = 1024	# (change requires restart)
  #update_process_title = on
  #stats_temp_directory = 'pg_stat_tmp'
  
***************
*** 465,470 ****
--- 465,473 ----
  #autovacuum_analyze_scale_factor = 0.1	# fraction of table size before analyze
  #autovacuum_freeze_max_age = 200000000	# maximum XID age before forced vacuum
  					# (change requires restart)
+ #autovacuum_multixact_freeze_max_age = 400000000	# maximum Multixact age
+ 					# before forced vacuum
+ 					# (change requires restart)
  #autovacuum_vacuum_cost_delay = 20ms	# default vacuum cost delay for
  					# autovacuum, in milliseconds;
  					# -1 means use vacuum_cost_delay
***************
*** 492,497 ****
--- 495,502 ----
  #lock_timeout = 0			# in milliseconds, 0 is disabled
  #vacuum_freeze_min_age = 50000000
  #vacuum_freeze_table_age = 150000000
+ #vacuum_multixact_freeze_min_age = 5000000
+ #vacuum_multixact_freeze_table_age = 150000000
  #bytea_output = 'hex'			# hex, escape
  #xmlbinary = 'base64'
  #xmloption = 'content'
*** a/src/include/commands/cluster.h
--- b/src/include/commands/cluster.h
***************
*** 20,26 ****
  
  extern void cluster(ClusterStmt *stmt, bool isTopLevel);
  extern void cluster_rel(Oid tableOid, Oid indexOid, bool recheck,
! 			bool verbose, int freeze_min_age, int freeze_table_age);
  extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
  						   bool recheck, LOCKMODE lockmode);
  extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
--- 20,27 ----
  
  extern void cluster(ClusterStmt *stmt, bool isTopLevel);
  extern void cluster_rel(Oid tableOid, Oid indexOid, bool recheck,
! 			bool verbose, int freeze_min_age, int freeze_table_age,
! 			int multixact_freeze_min_age, int multixact_freeze_table_age);
  extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
  						   bool recheck, LOCKMODE lockmode);
  extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
***************
*** 136,141 **** extern PGDLLIMPORT int default_statistics_target;		/* PGDLLIMPORT for
--- 136,143 ----
  														 * PostGIS */
  extern int	vacuum_freeze_min_age;
  extern int	vacuum_freeze_table_age;
+ extern int	vacuum_multixact_freeze_min_age;
+ extern int	vacuum_multixact_freeze_table_age;
  
  
  /* in commands/vacuum.c */
***************
*** 156,161 **** extern void vac_update_relstats(Relation relation,
--- 158,165 ----
  					TransactionId frozenxid,
  					MultiXactId minmulti);
  extern void vacuum_set_xid_limits(int freeze_min_age, int freeze_table_age,
+ 					  int multixact_freeze_min_age,
+ 					  int multixact_freeze_table_age,
  					  bool sharedRel,
  					  TransactionId *oldestXmin,
  					  TransactionId *freezeLimit,
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2430,2435 **** typedef struct VacuumStmt
--- 2430,2437 ----
  	int			options;		/* OR of VacuumOption flags */
  	int			freeze_min_age; /* min freeze age, or -1 to use default */
  	int			freeze_table_age;		/* age at which to scan whole table */
+ 	int			multixact_freeze_min_age; /* min multixact freeze age, or -1 to use default */
+ 	int			multixact_freeze_table_age; /* multixact age at which to scan whole table */
  	RangeVar   *relation;		/* single table to process, or NULL */
  	List	   *va_cols;		/* list of column names, or NIL for all */
  } VacuumStmt;
*** a/src/include/postmaster/autovacuum.h
--- b/src/include/postmaster/autovacuum.h
***************
*** 24,29 **** extern double autovacuum_vac_scale;
--- 24,30 ----
  extern int	autovacuum_anl_thresh;
  extern double autovacuum_anl_scale;
  extern int	autovacuum_freeze_max_age;
+ extern int	autovacuum_multixact_freeze_max_age;
  extern int	autovacuum_vac_cost_delay;
  extern int	autovacuum_vac_cost_limit;
  
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 198,203 **** typedef struct AutoVacOpts
--- 198,206 ----
  	int			freeze_min_age;
  	int			freeze_max_age;
  	int			freeze_table_age;
+ 	int			multixact_freeze_min_age;
+ 	int			multixact_freeze_max_age;
+ 	int			multixact_freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
  } AutoVacOpts;
#23Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alvaro Herrera (#22)
Re: truncating pg_multixact/members

Alvaro Herrera <alvherre@2ndquadrant.com> writes:

In this new version, I added a couple of fields to VacuumStmt node. How
strongly do we feel this would cause an ABI break? Would we be more
comfortable if I put them at the end of the struct for 9.3 instead?

In the past we've usually added such members at the end of the struct
in back branches (but put them in the logical place in HEAD). I'd
recommend doing that just on principle.

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Tom Lane (#23)
1 attachment(s)
Re: truncating pg_multixact/members

Tom Lane escribi�:

Alvaro Herrera <alvherre@2ndquadrant.com> writes:

In this new version, I added a couple of fields to VacuumStmt node. How
strongly do we feel this would cause an ABI break? Would we be more
comfortable if I put them at the end of the struct for 9.3 instead?

In the past we've usually added such members at the end of the struct
in back branches (but put them in the logical place in HEAD). I'd
recommend doing that just on principle.

Okay.

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

So can we assume that security_barrier is the only thing to be concerned
about? If so, the attached patch should work around the issue by
placing it in the same physical location. I guess if there are modules
that add extra stuff beyond StdRdOptions, this wouldn't work, but I'm
not really sure how likely this is given that our reloptions design
hasn't proven to be the most extensible thing in the world.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

preserve-security-barrier.patchtext/x-diff; charset=us-asciiDownload
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 187,193 **** typedef struct RelationData
   * be applied to relations that use this format or a superset for
   * private options data.
   */
!  /* autovacuum-related reloptions. */
  typedef struct AutoVacOpts
  {
  	bool		enabled;
--- 187,203 ----
   * be applied to relations that use this format or a superset for
   * private options data.
   */
!  /*
!   * autovacuum-related reloptions.
!   *
!   * In 9.3 starting from 9.3.3 we use a different struct definition,
!   * accomodating security_barrier inside the autovacuum struct, so that new
!   * fields could be added at the end.  This is so that third-party modules
!   * compiled with the original definition of the struct can continue to
!   * access the security_barrier field in its original physical location,
!   * avoiding an ABI break.  (9.4 and up use the normal definition, where
!   * security_barrier is placed outside autovacuum options.)
!   */
  typedef struct AutoVacOpts
  {
  	bool		enabled;
***************
*** 200,213 **** typedef struct AutoVacOpts
  	int			freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
  } AutoVacOpts;
  
  typedef struct StdRdOptions
  {
  	int32		vl_len_;		/* varlena header (do not touch directly!) */
  	int			fillfactor;		/* page fill factor in percent (0..100) */
! 	AutoVacOpts autovacuum;		/* autovacuum-related options */
! 	bool		security_barrier;		/* for views */
  } StdRdOptions;
  
  #define HEAP_MIN_FILLFACTOR			10
--- 210,226 ----
  	int			freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
+ 	bool		security_barrier;
+ 	int			multixact_freeze_min_age;
+ 	int			multixact_freeze_max_age;
+ 	int			multixact_freeze_table_age;
  } AutoVacOpts;
  
  typedef struct StdRdOptions
  {
  	int32		vl_len_;		/* varlena header (do not touch directly!) */
  	int			fillfactor;		/* page fill factor in percent (0..100) */
! 	AutoVacOpts	autovacuum;		/* autovacuum -- includes security_barrier */
  } StdRdOptions;
  
  #define HEAP_MIN_FILLFACTOR			10
***************
*** 238,247 **** typedef struct StdRdOptions
  /*
   * RelationIsSecurityView
   *		Returns whether the relation is security view, or not
   */
  #define RelationIsSecurityView(relation)	\
  	((relation)->rd_options ?				\
! 	 ((StdRdOptions *) (relation)->rd_options)->security_barrier : false)
  
  /*
   * RelationIsValid
--- 251,264 ----
  /*
   * RelationIsSecurityView
   *		Returns whether the relation is security view, or not
+  *
+  * XXX this definition copes with unusual placement of the field to preserve
+  * ABI compatibility with releases prior to 9.3.3.
   */
  #define RelationIsSecurityView(relation)	\
  	((relation)->rd_options ?				\
! 	 ((StdRdOptions *) (relation)->rd_options)->autovacuum.security_barrier : \
! 	 false)
  
  /*
   * RelationIsValid
#25Andres Freund
andres@2ndquadrant.com
In reply to: Alvaro Herrera (#24)
Re: truncating pg_multixact/members

On 2014-02-12 17:40:44 -0300, Alvaro Herrera wrote:

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

So can we assume that security_barrier is the only thing to be concerned
about? If so, the attached patch should work around the issue by
placing it in the same physical location.

Aw. How instead about temporarily introducing AutoVacMXactOpts or
something? Changing the name of the member variable sounds just as
likely to break things.

I guess if there are modules
that add extra stuff beyond StdRdOptions, this wouldn't work, but I'm
not really sure how likely this is given that our reloptions design
hasn't proven to be the most extensible thing in the world.

Hm, I don't see how it'd be problematic, even if they do. I don't really
understand the design of the reloptions code, but afaics, they shouldn't
do so by casting around rd_options but by parsing it anew, right?

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Andres Freund (#25)
Re: truncating pg_multixact/members

Andres Freund escribi�:

On 2014-02-12 17:40:44 -0300, Alvaro Herrera wrote:

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

So can we assume that security_barrier is the only thing to be concerned
about? If so, the attached patch should work around the issue by
placing it in the same physical location.

Aw. How instead about temporarily introducing AutoVacMXactOpts or
something? Changing the name of the member variable sounds just as
likely to break things.

Yes, that's what I did --- see the attached patch, which I would apply
on top of the code for master and would be only in 9.3. The idea here
is to keep the existing bits of StdRdOpts identical, so that macros such
as RelationIsSecurityView() that were compiled with the old rel.h
continue to work unchanged and without requiring a recompile.

I guess if there are modules
that add extra stuff beyond StdRdOptions, this wouldn't work, but I'm
not really sure how likely this is given that our reloptions design
hasn't proven to be the most extensible thing in the world.

Hm, I don't see how it'd be problematic, even if they do. I don't really
understand the design of the reloptions code, but afaics, they shouldn't
do so by casting around rd_options but by parsing it anew, right?

Now that I think about it, I don't think adding stuff at the end of
StdRdOptions has anything to do with adding nonstandard options. So if
we extend that struct we're not breaking any ABI contract.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#27Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#26)
1 attachment(s)
Re: truncating pg_multixact/members

Alvaro Herrera escribi�:

Yes, that's what I did --- see the attached patch, which I would apply
on top of the code for master and would be only in 9.3.

(Of course, these changes affect other parts of the code, in particular
autovacuum.c and reloptions.c. But that's not important here).

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

mxact-freeze-age-tweak.patchtext/x-diff; charset=us-asciiDownload
commit 689869c791aa7d13f408430ef438291bddb84b2f
Author: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date:   Thu Feb 13 12:31:37 2014 -0300

    9.3 tweaks to avoid ABI break

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 534a9ea..6daa14a 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -1170,11 +1170,11 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		{"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, freeze_table_age)},
 		{"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
-		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_min_age)},
+		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_min_age)},
 		{"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
-		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_max_age)},
+		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_max_age)},
 		{"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
-		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_table_age)},
+		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_table_age)},
 		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
 		{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d749f70..8ef59e6 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2430,10 +2430,11 @@ typedef struct VacuumStmt
 	int			options;		/* OR of VacuumOption flags */
 	int			freeze_min_age; /* min freeze age, or -1 to use default */
 	int			freeze_table_age;		/* age at which to scan whole table */
-	int			multixact_freeze_min_age; /* min multixact freeze age, or -1 to use default */
-	int			multixact_freeze_table_age; /* multixact age at which to scan whole table */
 	RangeVar   *relation;		/* single table to process, or NULL */
 	List	   *va_cols;		/* list of column names, or NIL for all */
+	/* place these at the end, to avoid ABI break within 9.3 branch */
+	int			multixact_freeze_min_age; /* min multixact freeze age, or -1 to use default */
+	int			multixact_freeze_table_age; /* multixact age at which to scan whole table */
 } VacuumStmt;
 
 /* ----------------------
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 47ae106..53d5690 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -187,7 +187,10 @@ typedef struct RelationData
  * be applied to relations that use this format or a superset for
  * private options data.
  */
- /* autovacuum-related reloptions. */
+ /* autovacuum-related reloptions.
+  *
+  * Split in two to avoid ABI break.
+  */
 typedef struct AutoVacOpts
 {
 	bool		enabled;
@@ -198,19 +201,24 @@ typedef struct AutoVacOpts
 	int			freeze_min_age;
 	int			freeze_max_age;
 	int			freeze_table_age;
-	int			multixact_freeze_min_age;
-	int			multixact_freeze_max_age;
-	int			multixact_freeze_table_age;
 	float8		vacuum_scale_factor;
 	float8		analyze_scale_factor;
 } AutoVacOpts;
 
+typedef struct AutoVacOpts2
+{
+	  int		multixact_freeze_min_age;
+	  int		multixact_freeze_max_age;
+	  int		multixact_freeze_table_age;
+} AutoVacOpts2;
+
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		security_barrier;		/* for views */
+	AutoVacOpts2 autovacuum2;	/* rest of autovacuum options */
 } StdRdOptions;
 
 #define HEAP_MIN_FILLFACTOR			10
#28Andres Freund
andres@2ndquadrant.com
In reply to: Alvaro Herrera (#26)
Re: truncating pg_multixact/members

On 2014-02-13 14:40:39 -0300, Alvaro Herrera wrote:

Andres Freund escribió:

On 2014-02-12 17:40:44 -0300, Alvaro Herrera wrote:

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

So can we assume that security_barrier is the only thing to be concerned
about? If so, the attached patch should work around the issue by
placing it in the same physical location.

Aw. How instead about temporarily introducing AutoVacMXactOpts or
something? Changing the name of the member variable sounds just as
likely to break things.

Yes, that's what I did --- see the attached patch, which I would apply
on top of the code for master and would be only in 9.3. The idea here
is to keep the existing bits of StdRdOpts identical, so that macros such
as RelationIsSecurityView() that were compiled with the old rel.h
continue to work unchanged and without requiring a recompile.

What I mean is that earlier code using StdRelOptions->security_barrier
directly now won't compile anymore. So you've changed a ABI breakage
into a API break. That's why I suggest adding the new options into a
separate struct at the end of StdRelOptions, that won't break anything.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#29Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Andres Freund (#25)
2 attachment(s)
Re: truncating pg_multixact/members

Andres Freund escribi�:

On 2014-02-12 17:40:44 -0300, Alvaro Herrera wrote:

Also, AutoVacOpts (used as part of reloptions) gained three extra
fields. Since this is in the middle of StdRdOptions, it'd be somewhat
more involve to put these at the end of that struct. This might be a
problem if somebody has a module calling RelationIsSecurityView(). If
anyone thinks we should be concerned about such an ABI change, please
shout quickly.

That sounds problematic --- surely StdRdOptions might be something
extensions are making use of?

So can we assume that security_barrier is the only thing to be concerned
about? If so, the attached patch should work around the issue by
placing it in the same physical location.

Aw. How instead about temporarily introducing AutoVacMXactOpts or
something? Changing the name of the member variable sounds just as
likely to break things.

So here are two patches -- the first one, for 9.3 and HEAD, introduce
the new aging variables and use them throughout vacuum and autovacuum,
including per-table options; the second one adjusts the struct
declarations to avoid the ABI break in VacuumStmt and StdRdOptions.

(Actually, for HEAD I needed to fix a failed merge due to the removal of
freeze age params to cluster_rel in commit 3cff1879f, but there's
nothing interesting there so I'm not posting that part.)

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

mxact-freeze-age-4.patchtext/x-diff; charset=us-asciiDownload
*** a/doc/src/sgml/config.sgml
--- b/doc/src/sgml/config.sgml
***************
*** 4730,4735 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 4730,4762 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
+       <term><varname>autovacuum_multixact_freeze_max_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>autovacuum_multixact_freeze_max_age</varname> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         Specifies the maximum age (in multixacts) that a table's
+         <structname>pg_class</>.<structfield>relminmxid</> field can
+         attain before a <command>VACUUM</> operation is forced to
+         prevent multixact ID wraparound within the table.
+         Note that the system will launch autovacuum processes to
+         prevent wraparound even when autovacuum is otherwise disabled.
+        </para>
+ 
+        <para>
+         Vacuuming multixacts also allows removal of old files from the
+         <filename>pg_multixact/members</> and <filename>pg_multixact/offsets</>
+         subdirectories, which is why the default is a relatively low
+         400 million multixacts.
+         This parameter can only be set at server start, but the setting
+         can be reduced for individual tables by changing storage parameters.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
        <term><varname>autovacuum_vacuum_cost_delay</varname> (<type>integer</type>)</term>
        <indexterm>
***************
*** 5138,5144 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
          <structname>pg_class</>.<structfield>relfrozenxid</> field has reached
          the age specified by this setting.  The default is 150 million
          transactions.  Although users can set this value anywhere from zero to
!         one billion, <command>VACUUM</> will silently limit the effective value
          to 95% of <xref linkend="guc-autovacuum-freeze-max-age">, so that a
          periodical manual <command>VACUUM</> has a chance to run before an
          anti-wraparound autovacuum is launched for the table. For more
--- 5165,5171 ----
          <structname>pg_class</>.<structfield>relfrozenxid</> field has reached
          the age specified by this setting.  The default is 150 million
          transactions.  Although users can set this value anywhere from zero to
!         two billions, <command>VACUUM</> will silently limit the effective value
          to 95% of <xref linkend="guc-autovacuum-freeze-max-age">, so that a
          periodical manual <command>VACUUM</> has a chance to run before an
          anti-wraparound autovacuum is launched for the table. For more
***************
*** 5169,5174 **** COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
--- 5196,5242 ----
        </listitem>
       </varlistentry>
  
+      <varlistentry id="guc-vacuum-multixact-freeze-table-age" xreflabel="vacuum_multixact_freeze_table_age">
+       <term><varname>vacuum_multixact_freeze_table_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>vacuum_multixact_freeze_table_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         <command>VACUUM</> performs a whole-table scan if the table's
+         <structname>pg_class</>.<structfield>relminmxid</> field has reached
+         the age specified by this setting.  The default is 150 million multixacts.
+         Although users can set this value anywhere from zero to two billions,
+         <command>VACUUM</> will silently limit the effective value to 95% of
+         <xref linkend="guc-autovacuum-multixact-freeze-max-age">, so that a
+         periodical manual <command>VACUUM</> has a chance to run before an
+         anti-wraparound is launched for the table.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
+      <varlistentry id="guc-vacuum-multixact-freeze-min-age" xreflabel="vacuum_multixact_freeze_min_age">
+       <term><varname>vacuum_multixact_freeze_min_age</varname> (<type>integer</type>)</term>
+       <indexterm>
+        <primary><varname>vacuum_multixact_freeze_min_age</> configuration parameter</primary>
+       </indexterm>
+       <listitem>
+        <para>
+         Specifies the cutoff age (in multixacts) that <command>VACUUM</>
+         should use to decide whether to replace multixact IDs with a newer
+         transaction ID or multixact ID while scanning a table.  The default
+         is 5 million multixacts.
+         Although users can set this value anywhere from zero to one billion,
+         <command>VACUUM</> will silently limit the effective value to half
+         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age">,
+         so that there is not an unreasonably short time between forced
+         autovacuums.
+         For more information see <xref linkend="vacuum-for-multixact-wraparound">.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
       <varlistentry id="guc-bytea-output" xreflabel="bytea_output">
        <term><varname>bytea_output</varname> (<type>enum</type>)</term>
        <indexterm>
*** a/doc/src/sgml/maintenance.sgml
--- b/doc/src/sgml/maintenance.sgml
***************
*** 108,114 ****
  
       <listitem>
        <simpara>To protect against loss of very old data due to
!       <firstterm>transaction ID wraparound</>.</simpara>
       </listitem>
      </orderedlist>
  
--- 108,115 ----
  
       <listitem>
        <simpara>To protect against loss of very old data due to
!       <firstterm>transaction ID wraparound</> or
!       <firstterm>multixact ID wraparound</>.</simpara>
       </listitem>
      </orderedlist>
  
***************
*** 379,384 ****
--- 380,390 ----
      <secondary>wraparound</secondary>
     </indexterm>
  
+     <indexterm>
+      <primary>wraparound</primary>
+      <secondary>of transaction IDs</secondary>
+     </indexterm>
+ 
     <para>
      <productname>PostgreSQL</productname>'s MVCC transaction semantics
      depend on being able to compare transaction ID (<acronym>XID</>)
***************
*** 599,604 **** HINT:  Stop the postmaster and use a standalone backend to VACUUM in "mydb".
--- 605,658 ----
      page for details about using a single-user backend.
     </para>
  
+    <sect3 id="vacuum-for-multixact-wraparound">
+     <title>Multixacts and Wraparound</title>
+ 
+     <indexterm>
+      <primary>MultiXactId</primary>
+     </indexterm>
+ 
+     <indexterm>
+      <primary>wraparound</primary>
+      <secondary>of multixact IDs</secondary>
+     </indexterm>
+ 
+     <para>
+      <firstterm>Multixacts</> are used to implement row locking by
+      multiple transactions: since there is limited space in the tuple
+      header to store lock information, that information is stored as a
+      multixact separately in the <filename>pg_multixact</> subdirectory,
+      and only its ID is in the <structfield>xmax</> field
+      in the tuple header.
+      Similar to transaction IDs, multixact IDs are implemented as a
+      32-bit counter and corresponding storage, all of which requires
+      careful aging management, storage cleanup, and wraparound handling.
+     </para>
+ 
+     <para>
+      During a <command>VACUUM</> table scan, either partial or of the whole
+      table, any multixact ID older than
+      <xref linkend="guc-vacuum-multixact-freeze-min-age">
+      is replaced by a different value, which can be the zero value, a single
+      transaction ID, or a newer multixact ID.  For each table,
+      <structname>pg_class</>.<structfield>relminmxid</> stores the oldest
+      possible value still stored in any tuple of that table.  Every time this
+      value is older than
+      <xref linkend="guc-vacuum-multixact-freeze-table-age">, a whole-table
+      scan is forced.  Whole-table <command>VACUUM</> scans, regardless of
+      what causes them, enable advancing the value for that table.
+      Eventually, as all tables in all databases are scanned and their
+      oldest multixact values are advanced, on-disk storage for older
+      multixacts can be removed.
+     </para>
+ 
+     <para>
+      As a safety device, a whole-table vacuum scan will occur for any table
+      whose multixact-age is greater than
+      <xref linkend="guc-autovacuum-multixact-freeze-max-age">.
+      This will occur even if autovacuum is nominally disabled.
+     </para>
+    </sect3>
    </sect2>
  
    <sect2 id="autovacuum">
*** a/doc/src/sgml/ref/create_table.sgml
--- b/doc/src/sgml/ref/create_table.sgml
***************
*** 981,987 **** CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
       <para>
       Custom <xref linkend="guc-vacuum-freeze-min-age"> parameter. Note that
       autovacuum will ignore attempts to set a per-table
!      <literal>autovacuum_freeze_min_age</> larger than the half system-wide
       <xref linkend="guc-autovacuum-freeze-max-age"> setting.
       </para>
      </listitem>
--- 981,987 ----
       <para>
       Custom <xref linkend="guc-vacuum-freeze-min-age"> parameter. Note that
       autovacuum will ignore attempts to set a per-table
!      <literal>autovacuum_freeze_min_age</> larger than half the system-wide
       <xref linkend="guc-autovacuum-freeze-max-age"> setting.
       </para>
      </listitem>
***************
*** 1010,1015 **** CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
--- 1010,1052 ----
      </listitem>
     </varlistentry>
  
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-vacuum-multixact-freeze-min-age"> parameter.
+       Note that autovacuum will ignore attempts to set a per-table
+       <literal>autovacuum_multixact_freeze_min_age</> larger than half the
+       system-wide <xref linkend="guc-autovacuum-multixact-freeze-max-age">
+       setting.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-autovacuum-multixact-freeze-max-age"> parameter. Note
+       that autovacuum will ignore attempts to set a per-table
+       <literal>autovacuum_multixact_freeze_max_age</> larger than the
+       system-wide setting (it can only be set smaller).  Note that while you
+       can set <literal>autovacuum_multixact_freeze_max_age</> very small,
+       or even zero, this is usually unwise since it will force frequent
+       vacuuming.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
+    <varlistentry>
+     <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)</term>
+     <listitem>
+      <para>
+       Custom <xref linkend="guc-vacuum-multixact-freeze-table-age"> parameter.
+      </para>
+     </listitem>
+    </varlistentry>
+ 
     </variablelist>
  
    </refsect2>
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 164,169 **** static relopt_int intRelOpts[] =
--- 164,177 ----
  	},
  	{
  		{
+ 			"autovacuum_multixact_freeze_min_age",
+ 			"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		},
+ 		-1, 0, 1000000000
+ 	},
+ 	{
+ 		{
  			"autovacuum_freeze_max_age",
  			"Age at which to autovacuum a table to prevent transaction ID wraparound",
  			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
***************
*** 172,182 **** static relopt_int intRelOpts[] =
--- 180,205 ----
  	},
  	{
  		{
+ 			"autovacuum_multixact_freeze_max_age",
+ 			"Multixact age at which to autovacuum a table to prevent multixact wraparound",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		},
+ 		-1, 100000000, 2000000000
+ 	},
+ 	{
+ 		{
  			"autovacuum_freeze_table_age",
  			"Age at which VACUUM should perform a full table sweep to replace old Xid values with FrozenXID",
  			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
  		}, -1, 0, 2000000000
  	},
+ 	{
+ 		{
+ 			"autovacuum_multixact_freeze_table_age",
+ 			"Age of multixact at which VACUUM should perform a full table sweep to replace old multixact values with newer ones",
+ 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST
+ 		}, -1, 0, 2000000000
+ 	},
  	/* list terminator */
  	{{NULL}}
  };
***************
*** 1146,1151 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
--- 1169,1180 ----
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, freeze_max_age)},
  		{"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, freeze_table_age)},
+ 		{"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ 		{"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ 		{"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_table_age)},
  		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
  		{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
*** a/src/backend/access/transam/multixact.c
--- b/src/backend/access/transam/multixact.c
***************
*** 2055,2065 **** SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid)
  	Assert(MultiXactIdIsValid(oldest_datminmxid));
  
  	/*
! 	 * The place where we actually get into deep trouble is halfway around
! 	 * from the oldest potentially-existing XID/multi.	(This calculation is
! 	 * probably off by one or two counts for Xids, because the special XIDs
! 	 * reduce the size of the loop a little bit.  But we throw in plenty of
! 	 * slop below, so it doesn't matter.)
  	 */
  	multiWrapLimit = oldest_datminmxid + (MaxMultiXactId >> 1);
  	if (multiWrapLimit < FirstMultiXactId)
--- 2055,2067 ----
  	Assert(MultiXactIdIsValid(oldest_datminmxid));
  
  	/*
! 	 * Since multixacts wrap differently from transaction IDs, this logic is
! 	 * not entirely correct: in some scenarios we could go for longer than 2
! 	 * billion multixacts without seeing any data loss, and in some others we
! 	 * could get in trouble before that if the new pg_multixact/members data
! 	 * stomps on the previous cycle's data.  For lack of a better mechanism we
! 	 * use the same logic as for transaction IDs, that is, start taking action
! 	 * halfway around the oldest potentially-existing multixact.
  	 */
  	multiWrapLimit = oldest_datminmxid + (MaxMultiXactId >> 1);
  	if (multiWrapLimit < FirstMultiXactId)
***************
*** 2093,2104 **** SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid)
  
  	/*
  	 * We'll start trying to force autovacuums when oldest_datminmxid gets to
! 	 * be more than autovacuum_freeze_max_age mxids old.
  	 *
! 	 * It's a bit ugly to just reuse limits for xids that way, but it doesn't
! 	 * seem worth adding separate GUCs for that purpose.
  	 */
! 	multiVacLimit = oldest_datminmxid + autovacuum_freeze_max_age;
  	if (multiVacLimit < FirstMultiXactId)
  		multiVacLimit += FirstMultiXactId;
  
--- 2095,2107 ----
  
  	/*
  	 * We'll start trying to force autovacuums when oldest_datminmxid gets to
! 	 * be more than autovacuum_multixact_freeze_max_age mxids old.
  	 *
! 	 * Note: autovacuum_multixact_freeze_max_age is a PGC_POSTMASTER parameter
! 	 * so that we don't have to worry about dealing with on-the-fly changes in
! 	 * its value.  See SetTransactionIdLimit.
  	 */
! 	multiVacLimit = oldest_datminmxid + autovacuum_multixact_freeze_max_age;
  	if (multiVacLimit < FirstMultiXactId)
  		multiVacLimit += FirstMultiXactId;
  
*** a/src/backend/access/transam/varsup.c
--- b/src/backend/access/transam/varsup.c
***************
*** 313,319 **** SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid)
  	 * value.  It doesn't look practical to update shared state from a GUC
  	 * assign hook (too many processes would try to execute the hook,
  	 * resulting in race conditions as well as crashes of those not connected
! 	 * to shared memory).  Perhaps this can be improved someday.
  	 */
  	xidVacLimit = oldest_datfrozenxid + autovacuum_freeze_max_age;
  	if (xidVacLimit < FirstNormalTransactionId)
--- 313,320 ----
  	 * value.  It doesn't look practical to update shared state from a GUC
  	 * assign hook (too many processes would try to execute the hook,
  	 * resulting in race conditions as well as crashes of those not connected
! 	 * to shared memory).  Perhaps this can be improved someday.  See also
! 	 * SetMultiXactIdLimit.
  	 */
  	xidVacLimit = oldest_datfrozenxid + autovacuum_freeze_max_age;
  	if (xidVacLimit < FirstNormalTransactionId)
*** a/src/backend/commands/cluster.c
--- b/src/backend/commands/cluster.c
***************
*** 64,72 **** typedef struct
  
  
  static void rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age, bool verbose);
  static void copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age, bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti);
  static List *get_tables_to_cluster(MemoryContext cluster_context);
--- 64,76 ----
  
  
  static void rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age,
! 				 int multixact_freeze_min_age, int multixact_freeze_table_age,
! 				 bool verbose);
  static void copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age,
! 			   int multixact_freeze_min_age, int multixact_freeze_table_age,
! 			   bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti);
  static List *get_tables_to_cluster(MemoryContext cluster_context);
***************
*** 179,185 **** cluster(ClusterStmt *stmt, bool isTopLevel)
  		 * Do the job.  We use a -1 freeze_min_age to avoid having CLUSTER
  		 * freeze tuples earlier than a plain VACUUM would.
  		 */
! 		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1);
  	}
  	else
  	{
--- 183,189 ----
  		 * Do the job.  We use a -1 freeze_min_age to avoid having CLUSTER
  		 * freeze tuples earlier than a plain VACUUM would.
  		 */
! 		cluster_rel(tableOid, indexOid, false, stmt->verbose, -1, -1, -1, -1);
  	}
  	else
  	{
***************
*** 230,236 **** cluster(ClusterStmt *stmt, bool isTopLevel)
  			PushActiveSnapshot(GetTransactionSnapshot());
  			/* Do the job.  As above, use a -1 freeze_min_age. */
  			cluster_rel(rvtc->tableOid, rvtc->indexOid, true, stmt->verbose,
! 						-1, -1);
  			PopActiveSnapshot();
  			CommitTransactionCommand();
  		}
--- 234,240 ----
  			PushActiveSnapshot(GetTransactionSnapshot());
  			/* Do the job.  As above, use a -1 freeze_min_age. */
  			cluster_rel(rvtc->tableOid, rvtc->indexOid, true, stmt->verbose,
! 						-1, -1, -1, -1);
  			PopActiveSnapshot();
  			CommitTransactionCommand();
  		}
***************
*** 262,268 **** cluster(ClusterStmt *stmt, bool isTopLevel)
   */
  void
  cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
! 			int freeze_min_age, int freeze_table_age)
  {
  	Relation	OldHeap;
  
--- 266,273 ----
   */
  void
  cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
! 			int freeze_min_age, int freeze_table_age,
! 			int multixact_freeze_min_age, int multixact_freeze_table_age)
  {
  	Relation	OldHeap;
  
***************
*** 407,412 **** cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose,
--- 412,418 ----
  
  	/* rebuild_relation does all the dirty work */
  	rebuild_relation(OldHeap, indexOid, freeze_min_age, freeze_table_age,
+ 					 multixact_freeze_min_age, multixact_freeze_table_age,
  					 verbose);
  
  	/* NB: rebuild_relation does heap_close() on OldHeap */
***************
*** 566,572 **** mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
   */
  static void
  rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age, bool verbose)
  {
  	Oid			tableOid = RelationGetRelid(OldHeap);
  	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
--- 572,580 ----
   */
  static void
  rebuild_relation(Relation OldHeap, Oid indexOid,
! 				 int freeze_min_age, int freeze_table_age,
! 				 int multixact_freeze_min_age, int multixact_freeze_table_age,
! 				 bool verbose)
  {
  	Oid			tableOid = RelationGetRelid(OldHeap);
  	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
***************
*** 591,597 **** rebuild_relation(Relation OldHeap, Oid indexOid,
  
  	/* Copy the heap data into the new table in the desired order */
  	copy_heap_data(OIDNewHeap, tableOid, indexOid,
! 				   freeze_min_age, freeze_table_age, verbose,
  				   &swap_toast_by_content, &frozenXid, &cutoffMulti);
  
  	/*
--- 599,607 ----
  
  	/* Copy the heap data into the new table in the desired order */
  	copy_heap_data(OIDNewHeap, tableOid, indexOid,
! 				   freeze_min_age, freeze_table_age,
! 				   multixact_freeze_min_age, multixact_freeze_table_age,
! 				   verbose,
  				   &swap_toast_by_content, &frozenXid, &cutoffMulti);
  
  	/*
***************
*** 733,739 **** make_new_heap(Oid OIDOldHeap, Oid NewTableSpace)
   */
  static void
  copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age, bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti)
  {
--- 743,751 ----
   */
  static void
  copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
! 			   int freeze_min_age, int freeze_table_age,
! 			   int multixact_freeze_min_age, int multixact_freeze_table_age,
! 			   bool verbose,
  			   bool *pSwapToastByContent, TransactionId *pFreezeXid,
  			   MultiXactId *pCutoffMulti)
  {
***************
*** 849,854 **** copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
--- 861,867 ----
  	 * compute xids used to freeze and weed out dead tuples.
  	 */
  	vacuum_set_xid_limits(freeze_min_age, freeze_table_age,
+ 						  multixact_freeze_min_age, multixact_freeze_table_age,
  						  OldHeap->rd_rel->relisshared,
  						  &OldestXmin, &FreezeXid, NULL, &MultiXactCutoff,
  						  NULL);
*** a/src/backend/commands/vacuum.c
--- b/src/backend/commands/vacuum.c
***************
*** 55,60 ****
--- 55,62 ----
   */
  int			vacuum_freeze_min_age;
  int			vacuum_freeze_table_age;
+ int			vacuum_multixact_freeze_min_age;
+ int			vacuum_multixact_freeze_table_age;
  
  
  /* A few variables that don't seem worth passing around as parameters */
***************
*** 398,403 **** get_rel_oids(Oid relid, const RangeVar *vacrel)
--- 400,407 ----
  void
  vacuum_set_xid_limits(int freeze_min_age,
  					  int freeze_table_age,
+ 					  int multixact_freeze_min_age,
+ 					  int multixact_freeze_table_age,
  					  bool sharedRel,
  					  TransactionId *oldestXmin,
  					  TransactionId *freezeLimit,
***************
*** 406,414 **** vacuum_set_xid_limits(int freeze_min_age,
  					  MultiXactId *mxactFullScanLimit)
  {
  	int			freezemin;
  	TransactionId limit;
  	TransactionId safeLimit;
! 	MultiXactId	mxactLimit;
  
  	/*
  	 * We can always ignore processes running lazy vacuum.	This is because we
--- 410,420 ----
  					  MultiXactId *mxactFullScanLimit)
  {
  	int			freezemin;
+ 	int			mxid_freezemin;
  	TransactionId limit;
  	TransactionId safeLimit;
! 	MultiXactId mxactLimit;
! 	MultiXactId safeMxactLimit;
  
  	/*
  	 * We can always ignore processes running lazy vacuum.	This is because we
***************
*** 462,474 **** vacuum_set_xid_limits(int freeze_min_age,
  	*freezeLimit = limit;
  
  	/*
! 	 * simplistic MultiXactId removal limit: use the same policy as for
! 	 * freezing Xids (except we use the oldest known mxact instead of the
! 	 * current next value).
  	 */
! 	mxactLimit = GetOldestMultiXactId() - freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
  	*multiXactCutoff = mxactLimit;
  
  	if (xidFullScanLimit != NULL)
--- 468,503 ----
  	*freezeLimit = limit;
  
  	/*
! 	 * Determine the minimum multixact freeze age to use: as specified by
! 	 * caller, or vacuum_multixact_freeze_min_age, but in any case not more
! 	 * than half autovacuum_multixact_freeze_max_age, so that autovacuums to
! 	 * prevent MultiXact wraparound won't occur too frequently.
  	 */
! 	mxid_freezemin = multixact_freeze_min_age;
! 	if (mxid_freezemin < 0)
! 		mxid_freezemin = vacuum_multixact_freeze_min_age;
! 	mxid_freezemin = Min(mxid_freezemin,
! 						 autovacuum_multixact_freeze_max_age / 2);
! 	Assert(mxid_freezemin >= 0);
! 
! 	/* compute the cutoff multi, being careful to generate a valid value */
! 	mxactLimit = GetOldestMultiXactId() - mxid_freezemin;
  	if (mxactLimit < FirstMultiXactId)
  		mxactLimit = FirstMultiXactId;
+ 
+ 	safeMxactLimit =
+ 		ReadNextMultiXactId() - autovacuum_multixact_freeze_max_age;
+ 	if (safeMxactLimit < FirstMultiXactId)
+ 		safeMxactLimit = FirstMultiXactId;
+ 
+ 	if (MultiXactIdPrecedes(mxactLimit, safeMxactLimit))
+ 	{
+ 		ereport(WARNING,
+ 				(errmsg("oldest multixact is far in the past"),
+ 				 errhint("Close open transactions with multixacts soon to avoid wraparound problems.")));
+ 		mxactLimit = safeMxactLimit;
+ 	}
+ 
  	*multiXactCutoff = mxactLimit;
  
  	if (xidFullScanLimit != NULL)
***************
*** 503,509 **** vacuum_set_xid_limits(int freeze_min_age,
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * (and limits) from plain XIDs here.
  		 */
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
--- 532,549 ----
  		/*
  		 * Compute MultiXactId limit to cause a full-table vacuum, being
  		 * careful not to generate an invalid multi. We just copy the logic
! 		 * from plain XIDs here.
! 		 */
! 		freezetable = multixact_freeze_table_age;
! 		if (freezetable < 0)
! 			freezetable = vacuum_multixact_freeze_table_age;
! 		freezetable = Min(freezetable,
! 						  autovacuum_multixact_freeze_max_age * 0.95);
! 		Assert(freezetable >= 0);
! 
! 		/*
! 		 * Compute MultiXact limit causing a full-table vacuum, being careful
! 		 * to generate a valid MultiXact value.
  		 */
  		mxactLimit = ReadNextMultiXactId() - freezetable;
  		if (mxactLimit < FirstMultiXactId)
***************
*** 511,516 **** vacuum_set_xid_limits(int freeze_min_age,
--- 551,560 ----
  
  		*mxactFullScanLimit = mxactLimit;
  	}
+ 	else
+ 	{
+ 		Assert(mxactFullScanLimit == NULL);
+ 	}
  }
  
  /*
***************
*** 1150,1156 **** vacuum_rel(Oid relid, VacuumStmt *vacstmt, bool do_toast, bool for_wraparound)
  		/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
  		cluster_rel(relid, InvalidOid, false,
  					(vacstmt->options & VACOPT_VERBOSE) != 0,
! 					vacstmt->freeze_min_age, vacstmt->freeze_table_age);
  	}
  	else
  		lazy_vacuum_rel(onerel, vacstmt, vac_strategy);
--- 1194,1202 ----
  		/* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
  		cluster_rel(relid, InvalidOid, false,
  					(vacstmt->options & VACOPT_VERBOSE) != 0,
! 					vacstmt->freeze_min_age, vacstmt->freeze_table_age,
! 					vacstmt->multixact_freeze_min_age,
! 					vacstmt->multixact_freeze_table_age);
  	}
  	else
  		lazy_vacuum_rel(onerel, vacstmt, vac_strategy);
*** a/src/backend/commands/vacuumlazy.c
--- b/src/backend/commands/vacuumlazy.c
***************
*** 203,208 **** lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt,
--- 203,210 ----
  	vac_strategy = bstrategy;
  
  	vacuum_set_xid_limits(vacstmt->freeze_min_age, vacstmt->freeze_table_age,
+ 						  vacstmt->multixact_freeze_min_age,
+ 						  vacstmt->multixact_freeze_table_age,
  						  onerel->rd_rel->relisshared,
  						  &OldestXmin, &FreezeLimit, &xidFullScanLimit,
  						  &MultiXactCutoff, &mxactFullScanLimit);
***************
*** 210,217 **** lazy_vacuum_rel(Relation onerel, VacuumStmt *vacstmt,
  	/*
  	 * We request a full scan if either the table's frozen Xid is now older
  	 * than or equal to the requested Xid full-table scan limit; or if the
! 	 * table's minimum MultiXactId is older than or equal to the requested mxid
! 	 * full-table scan limit.
  	 */
  	scan_all = TransactionIdPrecedesOrEquals(onerel->rd_rel->relfrozenxid,
  											 xidFullScanLimit);
--- 212,219 ----
  	/*
  	 * We request a full scan if either the table's frozen Xid is now older
  	 * than or equal to the requested Xid full-table scan limit; or if the
! 	 * table's minimum MultiXactId is older than or equal to the requested
! 	 * mxid full-table scan limit.
  	 */
  	scan_all = TransactionIdPrecedesOrEquals(onerel->rd_rel->relfrozenxid,
  											 xidFullScanLimit);
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
***************
*** 3206,3211 **** _copyVacuumStmt(const VacuumStmt *from)
--- 3206,3213 ----
  	COPY_SCALAR_FIELD(options);
  	COPY_SCALAR_FIELD(freeze_min_age);
  	COPY_SCALAR_FIELD(freeze_table_age);
+ 	COPY_SCALAR_FIELD(multixact_freeze_min_age);
+ 	COPY_SCALAR_FIELD(multixact_freeze_table_age);
  	COPY_NODE_FIELD(relation);
  	COPY_NODE_FIELD(va_cols);
  
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
***************
*** 1496,1501 **** _equalVacuumStmt(const VacuumStmt *a, const VacuumStmt *b)
--- 1496,1503 ----
  	COMPARE_SCALAR_FIELD(options);
  	COMPARE_SCALAR_FIELD(freeze_min_age);
  	COMPARE_SCALAR_FIELD(freeze_table_age);
+ 	COMPARE_SCALAR_FIELD(multixact_freeze_min_age);
+ 	COMPARE_SCALAR_FIELD(multixact_freeze_table_age);
  	COMPARE_NODE_FIELD(relation);
  	COMPARE_NODE_FIELD(va_cols);
  
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
***************
*** 8474,8479 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8474,8481 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8488,8493 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8490,8497 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					n->relation = $5;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8502,8507 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8506,8513 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = $3 ? 0 : -1;
  					n->freeze_table_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_min_age = $3 ? 0 : -1;
+ 					n->multixact_freeze_table_age = $3 ? 0 : -1;
  					$$ = (Node *)n;
  				}
  			| VACUUM '(' vacuum_option_list ')'
***************
*** 8509,8517 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8515,8531 ----
  					VacuumStmt *n = makeNode(VacuumStmt);
  					n->options = VACOPT_VACUUM | $3;
  					if (n->options & VACOPT_FREEZE)
+ 					{
  						n->freeze_min_age = n->freeze_table_age = 0;
+ 						n->multixact_freeze_min_age = 0;
+ 						n->multixact_freeze_table_age = 0;
+ 					}
  					else
+ 					{
  						n->freeze_min_age = n->freeze_table_age = -1;
+ 						n->multixact_freeze_min_age = -1;
+ 						n->multixact_freeze_table_age = -1;
+ 					}
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *) n;
***************
*** 8521,8529 **** VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
--- 8535,8551 ----
  					VacuumStmt *n = makeNode(VacuumStmt);
  					n->options = VACOPT_VACUUM | $3;
  					if (n->options & VACOPT_FREEZE)
+ 					{
  						n->freeze_min_age = n->freeze_table_age = 0;
+ 						n->multixact_freeze_min_age = 0;
+ 						n->multixact_freeze_table_age = 0;
+ 					}
  					else
+ 					{
  						n->freeze_min_age = n->freeze_table_age = -1;
+ 						n->multixact_freeze_min_age = -1;
+ 						n->multixact_freeze_table_age = -1;
+ 					}
  					n->relation = $5;
  					n->va_cols = $6;
  					if (n->va_cols != NIL)	/* implies analyze */
***************
*** 8553,8558 **** AnalyzeStmt:
--- 8575,8582 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = -1;
  					n->freeze_table_age = -1;
+ 					n->multixact_freeze_min_age = -1;
+ 					n->multixact_freeze_table_age = -1;
  					n->relation = NULL;
  					n->va_cols = NIL;
  					$$ = (Node *)n;
***************
*** 8565,8570 **** AnalyzeStmt:
--- 8589,8596 ----
  						n->options |= VACOPT_VERBOSE;
  					n->freeze_min_age = -1;
  					n->freeze_table_age = -1;
+ 					n->multixact_freeze_min_age = -1;
+ 					n->multixact_freeze_table_age = -1;
  					n->relation = $3;
  					n->va_cols = $4;
  					$$ = (Node *)n;
*** a/src/backend/postmaster/autovacuum.c
--- b/src/backend/postmaster/autovacuum.c
***************
*** 116,121 **** double		autovacuum_vac_scale;
--- 116,122 ----
  int			autovacuum_anl_thresh;
  double		autovacuum_anl_scale;
  int			autovacuum_freeze_max_age;
+ int			autovacuum_multixact_freeze_max_age;
  
  int			autovacuum_vac_cost_delay;
  int			autovacuum_vac_cost_limit;
***************
*** 144,149 **** static MultiXactId recentMulti;
--- 145,152 ----
  /* Default freeze ages to use for autovacuum (varies by database) */
  static int	default_freeze_min_age;
  static int	default_freeze_table_age;
+ static int	default_multixact_freeze_min_age;
+ static int	default_multixact_freeze_table_age;
  
  /* Memory context for long-lived data */
  static MemoryContext AutovacMemCxt;
***************
*** 185,190 **** typedef struct autovac_table
--- 188,195 ----
  	bool		at_doanalyze;
  	int			at_freeze_min_age;
  	int			at_freeze_table_age;
+ 	int			at_multixact_freeze_min_age;
+ 	int			at_multixact_freeze_table_age;
  	int			at_vacuum_cost_delay;
  	int			at_vacuum_cost_limit;
  	bool		at_wraparound;
***************
*** 1129,1135 **** do_start_worker(void)
  
  	/* Also determine the oldest datminmxid we will consider. */
  	recentMulti = ReadNextMultiXactId();
! 	multiForceLimit = recentMulti - autovacuum_freeze_max_age;
  	if (multiForceLimit < FirstMultiXactId)
  		multiForceLimit -= FirstMultiXactId;
  
--- 1134,1140 ----
  
  	/* Also determine the oldest datminmxid we will consider. */
  	recentMulti = ReadNextMultiXactId();
! 	multiForceLimit = recentMulti - autovacuum_multixact_freeze_max_age;
  	if (multiForceLimit < FirstMultiXactId)
  		multiForceLimit -= FirstMultiXactId;
  
***************
*** 1955,1965 **** do_autovacuum(void)
--- 1960,1974 ----
  	{
  		default_freeze_min_age = 0;
  		default_freeze_table_age = 0;
+ 		default_multixact_freeze_min_age = 0;
+ 		default_multixact_freeze_table_age = 0;
  	}
  	else
  	{
  		default_freeze_min_age = vacuum_freeze_min_age;
  		default_freeze_table_age = vacuum_freeze_table_age;
+ 		default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age;
+ 		default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
  	}
  
  	ReleaseSysCache(tuple);
***************
*** 2510,2515 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2519,2526 ----
  	{
  		int			freeze_min_age;
  		int			freeze_table_age;
+ 		int			multixact_freeze_min_age;
+ 		int			multixact_freeze_table_age;
  		int			vac_cost_limit;
  		int			vac_cost_delay;
  
***************
*** 2543,2554 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2554,2577 ----
  			? avopts->freeze_table_age
  			: default_freeze_table_age;
  
+ 		multixact_freeze_min_age = (avopts &&
+ 									avopts->multixact_freeze_min_age >= 0)
+ 			? avopts->multixact_freeze_min_age
+ 			: default_multixact_freeze_min_age;
+ 
+ 		multixact_freeze_table_age = (avopts &&
+ 									  avopts->multixact_freeze_table_age >= 0)
+ 			? avopts->multixact_freeze_table_age
+ 			: default_multixact_freeze_table_age;
+ 
  		tab = palloc(sizeof(autovac_table));
  		tab->at_relid = relid;
  		tab->at_dovacuum = dovacuum;
  		tab->at_doanalyze = doanalyze;
  		tab->at_freeze_min_age = freeze_min_age;
  		tab->at_freeze_table_age = freeze_table_age;
+ 		tab->at_multixact_freeze_min_age = multixact_freeze_min_age;
+ 		tab->at_multixact_freeze_table_age = multixact_freeze_table_age;
  		tab->at_vacuum_cost_limit = vac_cost_limit;
  		tab->at_vacuum_cost_delay = vac_cost_delay;
  		tab->at_wraparound = wraparound;
***************
*** 2567,2573 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
   *
   * Check whether a relation needs to be vacuumed or analyzed; return each into
   * "dovacuum" and "doanalyze", respectively.  Also return whether the vacuum is
!  * being forced because of Xid wraparound.
   *
   * relopts is a pointer to the AutoVacOpts options (either for itself in the
   * case of a plain table, or for either itself or its parent table in the case
--- 2590,2596 ----
   *
   * Check whether a relation needs to be vacuumed or analyzed; return each into
   * "dovacuum" and "doanalyze", respectively.  Also return whether the vacuum is
!  * being forced because of Xid or multixact wraparound.
   *
   * relopts is a pointer to the AutoVacOpts options (either for itself in the
   * case of a plain table, or for either itself or its parent table in the case
***************
*** 2586,2592 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
   * analyze.  This is asymmetric to the VACUUM case.
   *
   * We also force vacuum if the table's relfrozenxid is more than freeze_max_age
!  * transactions back.
   *
   * A table whose autovacuum_enabled option is false is
   * automatically skipped (unless we have to vacuum it due to freeze_max_age).
--- 2609,2616 ----
   * analyze.  This is asymmetric to the VACUUM case.
   *
   * We also force vacuum if the table's relfrozenxid is more than freeze_max_age
!  * transactions back, and if its relminmxid is more than
!  * multixact_freeze_max_age multixacts back.
   *
   * A table whose autovacuum_enabled option is false is
   * automatically skipped (unless we have to vacuum it due to freeze_max_age).
***************
*** 2628,2633 **** relation_needs_vacanalyze(Oid relid,
--- 2652,2658 ----
  
  	/* freeze parameters */
  	int			freeze_max_age;
+ 	int			multixact_freeze_max_age;
  	TransactionId xidForceLimit;
  	MultiXactId multiForceLimit;
  
***************
*** 2661,2666 **** relation_needs_vacanalyze(Oid relid,
--- 2686,2695 ----
  		? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
  		: autovacuum_freeze_max_age;
  
+ 	multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
+ 		  ? Min(relopts->multixact_freeze_max_age, autovacuum_multixact_freeze_max_age)
+ 		  : autovacuum_multixact_freeze_max_age;
+ 
  	av_enabled = (relopts ? relopts->enabled : true);
  
  	/* Force vacuum if table is at risk of wraparound */
***************
*** 2672,2678 **** relation_needs_vacanalyze(Oid relid,
  										  xidForceLimit));
  	if (!force_vacuum)
  	{
! 		multiForceLimit = recentMulti - autovacuum_freeze_max_age;
  		if (multiForceLimit < FirstMultiXactId)
  			multiForceLimit -= FirstMultiXactId;
  		force_vacuum = MultiXactIdPrecedes(classForm->relminmxid,
--- 2701,2707 ----
  										  xidForceLimit));
  	if (!force_vacuum)
  	{
! 		multiForceLimit = recentMulti - multixact_freeze_max_age;
  		if (multiForceLimit < FirstMultiXactId)
  			multiForceLimit -= FirstMultiXactId;
  		force_vacuum = MultiXactIdPrecedes(classForm->relminmxid,
***************
*** 2754,2759 **** autovacuum_do_vac_analyze(autovac_table *tab,
--- 2783,2790 ----
  		vacstmt.options |= VACOPT_ANALYZE;
  	vacstmt.freeze_min_age = tab->at_freeze_min_age;
  	vacstmt.freeze_table_age = tab->at_freeze_table_age;
+ 	vacstmt.multixact_freeze_min_age = tab->at_multixact_freeze_min_age;
+ 	vacstmt.multixact_freeze_table_age = tab->at_multixact_freeze_table_age;
  	/* we pass the OID, but might need this anyway for an error message */
  	vacstmt.relation = &rangevar;
  	vacstmt.va_cols = NIL;
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
***************
*** 1907,1912 **** static struct config_int ConfigureNamesInt[] =
--- 1907,1932 ----
  	},
  
  	{
+ 		{"vacuum_multixact_freeze_min_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Minimum age at which VACUUM should freeze a MultiXactId in a table row."),
+ 			NULL
+ 		},
+ 		&vacuum_multixact_freeze_min_age,
+ 		5000000, 0, 1000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
+ 		{"vacuum_multixact_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ 			gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."),
+ 			NULL
+ 		},
+ 		&vacuum_multixact_freeze_table_age,
+ 		150000000, 0, 2000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 
+ 	{
  		{"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_MASTER,
  			gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."),
  			NULL
***************
*** 2296,2301 **** static struct config_int ConfigureNamesInt[] =
--- 2316,2331 ----
  		NULL, NULL, NULL
  	},
  	{
+ 		/* see varsup.c for why this is PGC_POSTMASTER not PGC_SIGHUP */
+ 		{"autovacuum_multixact_freeze_max_age", PGC_POSTMASTER, AUTOVACUUM,
+ 			gettext_noop("Multixact age at which to autovacuum a table to prevent multixact wraparound."),
+ 			NULL
+ 		},
+ 		&autovacuum_multixact_freeze_max_age,
+ 		400000000, 10000000, 2000000000,
+ 		NULL, NULL, NULL
+ 	},
+ 	{
  		/* see max_connections */
  		{"autovacuum_max_workers", PGC_POSTMASTER, AUTOVACUUM,
  			gettext_noop("Sets the maximum number of simultaneously running autovacuum worker processes."),
*** a/src/backend/utils/misc/postgresql.conf.sample
--- b/src/backend/utils/misc/postgresql.conf.sample
***************
*** 362,373 ****
  					#   panic
  
  #log_min_error_statement = error	# values in order of decreasing detail:
! 				 	#   debug5
  					#   debug4
  					#   debug3
  					#   debug2
  					#   debug1
! 				 	#   info
  					#   notice
  					#   warning
  					#   error
--- 362,373 ----
  					#   panic
  
  #log_min_error_statement = error	# values in order of decreasing detail:
! 					#   debug5
  					#   debug4
  					#   debug3
  					#   debug2
  					#   debug1
! 					#   info
  					#   notice
  					#   warning
  					#   error
***************
*** 431,437 ****
  #track_counts = on
  #track_io_timing = off
  #track_functions = none			# none, pl, all
! #track_activity_query_size = 1024 	# (change requires restart)
  #update_process_title = on
  #stats_temp_directory = 'pg_stat_tmp'
  
--- 431,437 ----
  #track_counts = on
  #track_io_timing = off
  #track_functions = none			# none, pl, all
! #track_activity_query_size = 1024	# (change requires restart)
  #update_process_title = on
  #stats_temp_directory = 'pg_stat_tmp'
  
***************
*** 465,470 ****
--- 465,473 ----
  #autovacuum_analyze_scale_factor = 0.1	# fraction of table size before analyze
  #autovacuum_freeze_max_age = 200000000	# maximum XID age before forced vacuum
  					# (change requires restart)
+ #autovacuum_multixact_freeze_max_age = 400000000	# maximum Multixact age
+ 					# before forced vacuum
+ 					# (change requires restart)
  #autovacuum_vacuum_cost_delay = 20ms	# default vacuum cost delay for
  					# autovacuum, in milliseconds;
  					# -1 means use vacuum_cost_delay
***************
*** 492,497 ****
--- 495,502 ----
  #lock_timeout = 0			# in milliseconds, 0 is disabled
  #vacuum_freeze_min_age = 50000000
  #vacuum_freeze_table_age = 150000000
+ #vacuum_multixact_freeze_min_age = 5000000
+ #vacuum_multixact_freeze_table_age = 150000000
  #bytea_output = 'hex'			# hex, escape
  #xmlbinary = 'base64'
  #xmloption = 'content'
*** a/src/include/commands/cluster.h
--- b/src/include/commands/cluster.h
***************
*** 20,26 ****
  
  extern void cluster(ClusterStmt *stmt, bool isTopLevel);
  extern void cluster_rel(Oid tableOid, Oid indexOid, bool recheck,
! 			bool verbose, int freeze_min_age, int freeze_table_age);
  extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
  						   bool recheck, LOCKMODE lockmode);
  extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
--- 20,27 ----
  
  extern void cluster(ClusterStmt *stmt, bool isTopLevel);
  extern void cluster_rel(Oid tableOid, Oid indexOid, bool recheck,
! 			bool verbose, int freeze_min_age, int freeze_table_age,
! 			int multixact_freeze_min_age, int multixact_freeze_table_age);
  extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
  						   bool recheck, LOCKMODE lockmode);
  extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
*** a/src/include/commands/vacuum.h
--- b/src/include/commands/vacuum.h
***************
*** 136,141 **** extern PGDLLIMPORT int default_statistics_target;		/* PGDLLIMPORT for
--- 136,143 ----
  														 * PostGIS */
  extern int	vacuum_freeze_min_age;
  extern int	vacuum_freeze_table_age;
+ extern int	vacuum_multixact_freeze_min_age;
+ extern int	vacuum_multixact_freeze_table_age;
  
  
  /* in commands/vacuum.c */
***************
*** 156,161 **** extern void vac_update_relstats(Relation relation,
--- 158,165 ----
  					TransactionId frozenxid,
  					MultiXactId minmulti);
  extern void vacuum_set_xid_limits(int freeze_min_age, int freeze_table_age,
+ 					  int multixact_freeze_min_age,
+ 					  int multixact_freeze_table_age,
  					  bool sharedRel,
  					  TransactionId *oldestXmin,
  					  TransactionId *freezeLimit,
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2430,2435 **** typedef struct VacuumStmt
--- 2430,2439 ----
  	int			options;		/* OR of VacuumOption flags */
  	int			freeze_min_age; /* min freeze age, or -1 to use default */
  	int			freeze_table_age;		/* age at which to scan whole table */
+ 	int			multixact_freeze_min_age;		/* min multixact freeze age,
+ 												 * or -1 to use default */
+ 	int			multixact_freeze_table_age;		/* multixact age at which to
+ 												 * scan whole table */
  	RangeVar   *relation;		/* single table to process, or NULL */
  	List	   *va_cols;		/* list of column names, or NIL for all */
  } VacuumStmt;
*** a/src/include/postmaster/autovacuum.h
--- b/src/include/postmaster/autovacuum.h
***************
*** 24,29 **** extern double autovacuum_vac_scale;
--- 24,30 ----
  extern int	autovacuum_anl_thresh;
  extern double autovacuum_anl_scale;
  extern int	autovacuum_freeze_max_age;
+ extern int	autovacuum_multixact_freeze_max_age;
  extern int	autovacuum_vac_cost_delay;
  extern int	autovacuum_vac_cost_limit;
  
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 198,203 **** typedef struct AutoVacOpts
--- 198,206 ----
  	int			freeze_min_age;
  	int			freeze_max_age;
  	int			freeze_table_age;
+ 	int			multixact_freeze_min_age;
+ 	int			multixact_freeze_max_age;
+ 	int			multixact_freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
  } AutoVacOpts;
mxact-freeze-age-4-9.3.tweak.patchtext/x-diff; charset=us-asciiDownload
*** a/src/backend/access/common/reloptions.c
--- b/src/backend/access/common/reloptions.c
***************
*** 1170,1180 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
  		{"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, freeze_table_age)},
  		{"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_min_age)},
  		{"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_max_age)},
  		{"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, multixact_freeze_table_age)},
  		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
  		{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
--- 1170,1180 ----
  		{"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, freeze_table_age)},
  		{"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_min_age)},
  		{"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_max_age)},
  		{"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
! 		offsetof(StdRdOptions, autovacuum2) +offsetof(AutoVacOpts2, multixact_freeze_table_age)},
  		{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
  		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
  		{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
*** a/src/backend/postmaster/autovacuum.c
--- b/src/backend/postmaster/autovacuum.c
***************
*** 302,307 **** static void FreeWorkerInfo(int code, Datum arg);
--- 302,308 ----
  static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
  					  TupleDesc pg_class_desc);
  static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+ 						  AutoVacOpts2 *relopts2,
  						  Form_pg_class classForm,
  						  PgStat_StatTabEntry *tabentry,
  						  bool *dovacuum, bool *doanalyze, bool *wraparound);
***************
*** 309,315 **** static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
  static void autovacuum_do_vac_analyze(autovac_table *tab,
  						  BufferAccessStrategy bstrategy);
  static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
! 					 TupleDesc pg_class_desc);
  static PgStat_StatTabEntry *get_pgstat_tabentry_relid(Oid relid, bool isshared,
  						  PgStat_StatDBEntry *shared,
  						  PgStat_StatDBEntry *dbentry);
--- 310,317 ----
  static void autovacuum_do_vac_analyze(autovac_table *tab,
  						  BufferAccessStrategy bstrategy);
  static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
! 					 TupleDesc pg_class_desc,
! 					 AutoVacOpts2 **opts2);
  static PgStat_StatTabEntry *get_pgstat_tabentry_relid(Oid relid, bool isshared,
  						  PgStat_StatDBEntry *shared,
  						  PgStat_StatDBEntry *dbentry);
***************
*** 2020,2025 **** do_autovacuum(void)
--- 2022,2028 ----
  		Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
  		PgStat_StatTabEntry *tabentry;
  		AutoVacOpts *relopts;
+ 		AutoVacOpts2 *relopts2;
  		Oid			relid;
  		bool		dovacuum;
  		bool		doanalyze;
***************
*** 2032,2043 **** do_autovacuum(void)
  		relid = HeapTupleGetOid(tuple);
  
  		/* Fetch reloptions and the pgstat entry for this table */
! 		relopts = extract_autovac_opts(tuple, pg_class_desc);
  		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  											 shared, dbentry);
  
  		/* Check if it needs vacuum or analyze */
! 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
  								  &dovacuum, &doanalyze, &wraparound);
  
  		/*
--- 2035,2046 ----
  		relid = HeapTupleGetOid(tuple);
  
  		/* Fetch reloptions and the pgstat entry for this table */
! 		relopts = extract_autovac_opts(tuple, pg_class_desc, &relopts2);
  		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  											 shared, dbentry);
  
  		/* Check if it needs vacuum or analyze */
! 		relation_needs_vacanalyze(relid, relopts, relopts2, classForm, tabentry,
  								  &dovacuum, &doanalyze, &wraparound);
  
  		/*
***************
*** 2134,2139 **** do_autovacuum(void)
--- 2137,2143 ----
  		PgStat_StatTabEntry *tabentry;
  		Oid			relid;
  		AutoVacOpts *relopts = NULL;
+ 		AutoVacOpts2 *relopts2 = NULL;
  		bool		dovacuum;
  		bool		doanalyze;
  		bool		wraparound;
***************
*** 2150,2156 **** do_autovacuum(void)
  		 * fetch reloptions -- if this toast table does not have them, try the
  		 * main rel
  		 */
! 		relopts = extract_autovac_opts(tuple, pg_class_desc);
  		if (relopts == NULL)
  		{
  			av_relation *hentry;
--- 2154,2160 ----
  		 * fetch reloptions -- if this toast table does not have them, try the
  		 * main rel
  		 */
! 		relopts = extract_autovac_opts(tuple, pg_class_desc, &relopts2);
  		if (relopts == NULL)
  		{
  			av_relation *hentry;
***************
*** 2165,2171 **** do_autovacuum(void)
  		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  											 shared, dbentry);
  
! 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
  								  &dovacuum, &doanalyze, &wraparound);
  
  		/* ignore analyze for toast tables */
--- 2169,2175 ----
  		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  											 shared, dbentry);
  
! 		relation_needs_vacanalyze(relid, relopts, relopts2, classForm, tabentry,
  								  &dovacuum, &doanalyze, &wraparound);
  
  		/* ignore analyze for toast tables */
***************
*** 2406,2417 **** deleted:
   *
   * Given a relation's pg_class tuple, return the AutoVacOpts portion of
   * reloptions, if set; otherwise, return NULL.
   */
  static AutoVacOpts *
! extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
  {
  	bytea	   *relopts;
  	AutoVacOpts *av;
  
  	Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
  		   ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
--- 2410,2426 ----
   *
   * Given a relation's pg_class tuple, return the AutoVacOpts portion of
   * reloptions, if set; otherwise, return NULL.
+  *
+  * 9.3 kludge: return the separate "AutoVacOpts2" part too.
   */
  static AutoVacOpts *
! extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc, AutoVacOpts2 **opts2)
  {
  	bytea	   *relopts;
  	AutoVacOpts *av;
+ 	AutoVacOpts2 *av2;
+ 
+ 	*opts2 = NULL;
  
  	Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
  		   ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
***************
*** 2423,2428 **** extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
--- 2432,2442 ----
  
  	av = palloc(sizeof(AutoVacOpts));
  	memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
+ 
+ 	av2 = palloc(sizeof(AutoVacOpts2));
+ 	memcpy(av2, &(((StdRdOptions *) relopts)->autovacuum2), sizeof(AutoVacOpts2));
+ 	*opts2 = av2;
+ 
  	pfree(relopts);
  
  	return av;
***************
*** 2474,2479 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2488,2494 ----
  	PgStat_StatDBEntry *dbentry;
  	bool		wraparound;
  	AutoVacOpts *avopts;
+ 	AutoVacOpts2 *avopts2;
  
  	/* use fresh stats */
  	autovac_refresh_stats();
***************
*** 2491,2497 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
  	 * Get the applicable reloptions.  If it is a TOAST table, try to get the
  	 * main table reloptions if the toast table itself doesn't have.
  	 */
! 	avopts = extract_autovac_opts(classTup, pg_class_desc);
  	if (classForm->relkind == RELKIND_TOASTVALUE &&
  		avopts == NULL && table_toast_map != NULL)
  	{
--- 2506,2512 ----
  	 * Get the applicable reloptions.  If it is a TOAST table, try to get the
  	 * main table reloptions if the toast table itself doesn't have.
  	 */
! 	avopts = extract_autovac_opts(classTup, pg_class_desc, &avopts2);
  	if (classForm->relkind == RELKIND_TOASTVALUE &&
  		avopts == NULL && table_toast_map != NULL)
  	{
***************
*** 2507,2513 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
  	tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  										 shared, dbentry);
  
! 	relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
  							  &dovacuum, &doanalyze, &wraparound);
  
  	/* ignore ANALYZE for toast tables */
--- 2522,2528 ----
  	tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
  										 shared, dbentry);
  
! 	relation_needs_vacanalyze(relid, avopts, avopts2, classForm, tabentry,
  							  &dovacuum, &doanalyze, &wraparound);
  
  	/* ignore ANALYZE for toast tables */
***************
*** 2554,2567 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
  			? avopts->freeze_table_age
  			: default_freeze_table_age;
  
! 		multixact_freeze_min_age = (avopts &&
! 									avopts->multixact_freeze_min_age >= 0)
! 			? avopts->multixact_freeze_min_age
  			: default_multixact_freeze_min_age;
  
! 		multixact_freeze_table_age = (avopts &&
! 									  avopts->multixact_freeze_table_age >= 0)
! 			? avopts->multixact_freeze_table_age
  			: default_multixact_freeze_table_age;
  
  		tab = palloc(sizeof(autovac_table));
--- 2569,2582 ----
  			? avopts->freeze_table_age
  			: default_freeze_table_age;
  
! 		multixact_freeze_min_age = (avopts2 &&
! 									avopts2->multixact_freeze_min_age >= 0)
! 			? avopts2->multixact_freeze_min_age
  			: default_multixact_freeze_min_age;
  
! 		multixact_freeze_table_age = (avopts2 &&
! 									  avopts2->multixact_freeze_table_age >= 0)
! 			? avopts2->multixact_freeze_table_age
  			: default_multixact_freeze_table_age;
  
  		tab = palloc(sizeof(autovac_table));
***************
*** 2625,2630 **** table_recheck_autovac(Oid relid, HTAB *table_toast_map,
--- 2640,2646 ----
  static void
  relation_needs_vacanalyze(Oid relid,
  						  AutoVacOpts *relopts,
+ 						  AutoVacOpts2 *relopts2,
  						  Form_pg_class classForm,
  						  PgStat_StatTabEntry *tabentry,
   /* output params below */
***************
*** 2686,2693 **** relation_needs_vacanalyze(Oid relid,
  		? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
  		: autovacuum_freeze_max_age;
  
! 	multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
! 		  ? Min(relopts->multixact_freeze_max_age, autovacuum_multixact_freeze_max_age)
  		  : autovacuum_multixact_freeze_max_age;
  
  	av_enabled = (relopts ? relopts->enabled : true);
--- 2702,2709 ----
  		? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
  		: autovacuum_freeze_max_age;
  
! 	multixact_freeze_max_age = (relopts2 && relopts2->multixact_freeze_max_age >= 0)
! 		  ? Min(relopts2->multixact_freeze_max_age, autovacuum_multixact_freeze_max_age)
  		  : autovacuum_multixact_freeze_max_age;
  
  	av_enabled = (relopts ? relopts->enabled : true);
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
***************
*** 2430,2441 **** typedef struct VacuumStmt
  	int			options;		/* OR of VacuumOption flags */
  	int			freeze_min_age; /* min freeze age, or -1 to use default */
  	int			freeze_table_age;		/* age at which to scan whole table */
  	int			multixact_freeze_min_age;		/* min multixact freeze age,
  												 * or -1 to use default */
  	int			multixact_freeze_table_age;		/* multixact age at which to
  												 * scan whole table */
- 	RangeVar   *relation;		/* single table to process, or NULL */
- 	List	   *va_cols;		/* list of column names, or NIL for all */
  } VacuumStmt;
  
  /* ----------------------
--- 2430,2442 ----
  	int			options;		/* OR of VacuumOption flags */
  	int			freeze_min_age; /* min freeze age, or -1 to use default */
  	int			freeze_table_age;		/* age at which to scan whole table */
+ 	RangeVar   *relation;		/* single table to process, or NULL */
+ 	List	   *va_cols;		/* list of column names, or NIL for all */
+ 	/* place these at the end, to avoid ABI break within 9.3 branch */
  	int			multixact_freeze_min_age;		/* min multixact freeze age,
  												 * or -1 to use default */
  	int			multixact_freeze_table_age;		/* multixact age at which to
  												 * scan whole table */
  } VacuumStmt;
  
  /* ----------------------
*** a/src/include/utils/rel.h
--- b/src/include/utils/rel.h
***************
*** 187,193 **** typedef struct RelationData
   * be applied to relations that use this format or a superset for
   * private options data.
   */
!  /* autovacuum-related reloptions. */
  typedef struct AutoVacOpts
  {
  	bool		enabled;
--- 187,197 ----
   * be applied to relations that use this format or a superset for
   * private options data.
   */
!  /*
!   * autovacuum-related reloptions.
!   *
!   * Split in two to avoid ABI break.
!   */
  typedef struct AutoVacOpts
  {
  	bool		enabled;
***************
*** 198,216 **** typedef struct AutoVacOpts
  	int			freeze_min_age;
  	int			freeze_max_age;
  	int			freeze_table_age;
- 	int			multixact_freeze_min_age;
- 	int			multixact_freeze_max_age;
- 	int			multixact_freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
  } AutoVacOpts;
  
  typedef struct StdRdOptions
  {
  	int32		vl_len_;		/* varlena header (do not touch directly!) */
  	int			fillfactor;		/* page fill factor in percent (0..100) */
  	AutoVacOpts autovacuum;		/* autovacuum-related options */
  	bool		security_barrier;		/* for views */
  } StdRdOptions;
  
  #define HEAP_MIN_FILLFACTOR			10
--- 202,231 ----
  	int			freeze_min_age;
  	int			freeze_max_age;
  	int			freeze_table_age;
  	float8		vacuum_scale_factor;
  	float8		analyze_scale_factor;
  } AutoVacOpts;
  
+ /*
+  * The multixact freeze parameters were added after 9.3.2 had been released;
+  * to preserve ABI compatibility with modules that might have been compiled
+  * prior to 9.3.3, these are placed in a separate struct so that they can be
+  * located at the end of the containing struct.
+  */
+ typedef struct AutoVacOpts2
+ {
+ 	int		multixact_freeze_min_age;
+ 	int		multixact_freeze_max_age;
+ 	int		multixact_freeze_table_age;
+ } AutoVacOpts2;
+ 
  typedef struct StdRdOptions
  {
  	int32		vl_len_;		/* varlena header (do not touch directly!) */
  	int			fillfactor;		/* page fill factor in percent (0..100) */
  	AutoVacOpts autovacuum;		/* autovacuum-related options */
  	bool		security_barrier;		/* for views */
+ 	AutoVacOpts2 autovacuum2;	/* rest of autovacuum options */
  } StdRdOptions;
  
  #define HEAP_MIN_FILLFACTOR			10
#30Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#29)
Re: truncating pg_multixact/members

Alvaro Herrera escribi�:

So here are two patches -- the first one, for 9.3 and HEAD, introduce
the new aging variables and use them throughout vacuum and autovacuum,
including per-table options; the second one adjusts the struct
declarations to avoid the ABI break in VacuumStmt and StdRdOptions.

I forgot to ask: what opinions are there about
vacuum_multixact_freeze_table_age's default value? Right now I have 150
million, same as for Xids. However, it might make sense to use 300
millions, so that whole-table scans are not forced earlier than for Xids
unless consumption rate for multixacts is double the one for Xids.

I already have set autovacuum_multixact_freeze_max_age to 400 million,
i.e. double that for Xids. This means emergency vacuums will not take
place for multis, unless consumption rate is double that for Xids.
This seems pretty reasonable to me.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#31Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#29)
Re: truncating pg_multixact/members

Alvaro Herrera escribi�:

So here are two patches -- the first one, for 9.3 and HEAD, introduce
the new aging variables and use them throughout vacuum and autovacuum,
including per-table options; the second one adjusts the struct
declarations to avoid the ABI break in VacuumStmt and StdRdOptions.

I have pushed this for both 9.3 and master.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers