Emmanuel Cecchet wrote:
Emmanuel Cecchet wrote:
As I have not found yet an elegant solution to deal with the DROP CASCADE issue, here is a simpler patch that handles temp tables that are dropped at commit time. This is a good first step and we will try to elaborate further to support ON COMMIT DELETE ROWS.

The problem with stopping the postmaster seems to still be there..

All the problems are centered around locking. We need to address that and decide what is sane locking behavior wrt. temp tables and 2PC.

First, there's the case where a temp table is created and dropped in the same transaction. It seems perfectly sane to me to simply drop all locks on the dropped table at PREPARE TRANSACTION. Does anyone see a problem with that? If not, we might as well do that for all tables, not just temporary ones. It seems just as safe for non-temporary tables.
This seems good to me. Any access to the table after PREPARE TRANSACTION but before COMMIT on that backend would return a relation not found which is what we expect. For a regular table, I don't know if that makes a difference if the prepared transaction rollbacks?

I don't think there's any difference with temp tables and regular ones from locking point of view.

Secondly, there's the case of accessing a ON COMMIT DELETE ROWS table. There too, could we simply drop the locks at PREPARE TRANSACTION? I can't immediately see anything wrong with that.
As there is no data anyway, I don't think the locks are going to change anything. But in the most recent stripped-down version of the patch, on delete rows is no more supported, I only allow on commit drop. I did not find the flag to see if a temp table was created with the on delete rows option.

Hmm. I think we can use the on_commits list in tablecmds.c for that.

Do you want me to look at the locking code or will you have time to do it? Hints will be welcome if you want me to do it.

I can give it a shot for change. Attached is a patch that allows the ON COMMIT DELETE ROWS case. The beef of the patch is:

- An entry is made into the on_commits list in tablecmds.c for all temp tables, even if there's no ON COMMIT action - There's a new function, check_prepare_safe_temp_table(Oid relid) in tablecmds.c, that uses the on_commits list to determine if the access to the given relation is "PREPARE-safe". That is, it's not a temp table, or it's an access to an ON COMMIT DELETE ROWS temp table and the temp table wasn't created or dropped in the same transaction. - MyXactMadeTempRelUpdate variable is gone. The check is driven from the lock manager again, like it was in 8.1, by calling the new check_prepare_sage_temp_table function for all relation locks in AtPrepare_Locks(). - changed the on_commits linked list in tablecmds.c into a hash table for performance

Somehow this feels pretty baroque, though. Perhaps a better approach would be to add a new AtPrepare_OnCommitActions function to tablecmds.c, that gets called before AtPrepare_Locks. It would scan through the on_commits list, and release all locks for the "PREPARE-safe" temp tables, and throw the error if necessary. I'll try that next.

BTW, there's a very relevant thread here:

http://archives.postgresql.org/pgsql-hackers/2008-03/msg00063.php

if you haven't read it already.

--
  Heikki Linnakangas
  EnterpriseDB   http://www.enterprisedb.com
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 33d5fab..b94e2bd 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -876,10 +876,6 @@ relation_open(Oid relationId, LOCKMODE lockmode)
 	if (!RelationIsValid(r))
 		elog(ERROR, "could not open relation with OID %u", relationId);
 
-	/* Make note that we've accessed a temporary relation */
-	if (r->rd_istemp)
-		MyXactAccessedTempRel = true;
-
 	pgstat_initstats(r);
 
 	return r;
@@ -924,10 +920,6 @@ try_relation_open(Oid relationId, LOCKMODE lockmode)
 	if (!RelationIsValid(r))
 		elog(ERROR, "could not open relation with OID %u", relationId);
 
-	/* Make note that we've accessed a temporary relation */
-	if (r->rd_istemp)
-		MyXactAccessedTempRel = true;
-
 	pgstat_initstats(r);
 
 	return r;
@@ -974,10 +966,6 @@ relation_open_nowait(Oid relationId, LOCKMODE lockmode)
 	if (!RelationIsValid(r))
 		elog(ERROR, "could not open relation with OID %u", relationId);
 
-	/* Make note that we've accessed a temporary relation */
-	if (r->rd_istemp)
-		MyXactAccessedTempRel = true;
-
 	pgstat_initstats(r);
 
 	return r;
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index b7d1285..c859874 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -67,14 +67,6 @@ int			CommitDelay = 0;	/* precommit delay in microseconds */
 int			CommitSiblings = 5; /* # concurrent xacts needed to sleep */
 
 /*
- * MyXactAccessedTempRel is set when a temporary relation is accessed.
- * We don't allow PREPARE TRANSACTION in that case.  (This is global
- * so that it can be set from heapam.c.)
- */
-bool		MyXactAccessedTempRel = false;
-
-
-/*
  *	transaction states - transaction state from server perspective
  */
 typedef enum TransState
@@ -1467,7 +1459,6 @@ StartTransaction(void)
 	XactIsoLevel = DefaultXactIsoLevel;
 	XactReadOnly = DefaultXactReadOnly;
 	forceSyncCommit = false;
-	MyXactAccessedTempRel = false;
 
 	/*
 	 * reinitialize within-transaction counters
@@ -1798,26 +1789,6 @@ PrepareTransaction(void)
 
 	/* NOTIFY and flatfiles will be handled below */
 
-	/*
-	 * Don't allow PREPARE TRANSACTION if we've accessed a temporary table
-	 * in this transaction.  Having the prepared xact hold locks on another
-	 * backend's temp table seems a bad idea --- for instance it would prevent
-	 * the backend from exiting.  There are other problems too, such as how
-	 * to clean up the source backend's local buffers and ON COMMIT state
-	 * if the prepared xact includes a DROP of a temp table.
-	 *
-	 * We must check this after executing any ON COMMIT actions, because
-	 * they might still access a temp relation.
-	 *
-	 * XXX In principle this could be relaxed to allow some useful special
-	 * cases, such as a temp table created and dropped all within the
-	 * transaction.  That seems to require much more bookkeeping though.
-	 */
-	if (MyXactAccessedTempRel)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot PREPARE a transaction that has operated on temporary tables")));
-
 	/* Prevent cancel/die interrupt while cleaning up */
 	HOLD_INTERRUPTS();
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index afc6977..99e941b 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -39,6 +39,7 @@
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_inherits.h"
@@ -1064,7 +1065,7 @@ heap_create_with_catalog(const char *relname,
 	/*
 	 * If there's a special on-commit action, remember it
 	 */
-	if (oncommit != ONCOMMIT_NOOP)
+	if (isTempOrToastNamespace(relnamespace))
 		register_on_commit_action(relid, oncommit);
 
 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6559470..f201e0d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -96,7 +96,7 @@ typedef struct OnCommitItem
 	SubTransactionId deleting_subid;
 } OnCommitItem;
 
-static List *on_commits = NIL;
+static HTAB *on_commits = NULL;
 
 
 /*
@@ -7575,26 +7575,38 @@ void
 register_on_commit_action(Oid relid, OnCommitAction action)
 {
 	OnCommitItem *oc;
-	MemoryContext oldcxt;
+	bool		  found;
 
 	/*
-	 * We needn't bother registering the relation unless there is an ON COMMIT
-	 * action we need to take.
+	 * We need to have an entry for all temporary tables, even for
+	 * no-op actions, see check_prepare_safe_temp_table().
 	 */
-	if (action == ONCOMMIT_NOOP || action == ONCOMMIT_PRESERVE_ROWS)
-		return;
 
-	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+	if (on_commits == NULL)
+	{
+		HASHCTL		hash_ctl;
+
+		/* Initialize hash tables used to track update chains */
+		memset(&hash_ctl, 0, sizeof(hash_ctl));
+		hash_ctl.keysize = sizeof(Oid);
+		hash_ctl.entrysize = sizeof(OnCommitItem);
+		hash_ctl.hcxt = CacheMemoryContext;
+		hash_ctl.hash = oid_hash;
+
+		on_commits = hash_create("On commit actions",
+								 4, /* small initial size to make scans fast */
+								 &hash_ctl,
+								 HASH_ELEM | HASH_FUNCTION | HASH_CONTEXT);
+	}
+
+	oc = (OnCommitItem *) hash_search(on_commits, &relid, HASH_ENTER, &found);
+	if (found)
+		elog(ERROR, "relid already in on commit action table");
 
-	oc = (OnCommitItem *) palloc(sizeof(OnCommitItem));
 	oc->relid = relid;
 	oc->oncommit = action;
 	oc->creating_subid = GetCurrentSubTransactionId();
 	oc->deleting_subid = InvalidSubTransactionId;
-
-	on_commits = lcons(oc, on_commits);
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
 /*
@@ -7605,17 +7617,90 @@ register_on_commit_action(Oid relid, OnCommitAction action)
 void
 remove_on_commit_action(Oid relid)
 {
-	ListCell   *l;
+	OnCommitItem *oc;
+
+	if (on_commits == NULL)
+		return;
 
-	foreach(l, on_commits)
+	oc = (OnCommitItem *) hash_search(on_commits, &relid, HASH_FIND, NULL);
+
+	if (oc != NULL)
+		oc->deleting_subid = GetCurrentSubTransactionId();
+}
+
+/*
+ * Don't allow PREPARE TRANSACTION if we've accessed a temporary table in
+ * this transaction.  Having the prepared xact hold locks on another
+ * backend's temp table seems a bad idea --- for instance it would prevent
+ * the backend from exiting.  There are other problems too, such as how
+ * to clean up the source backend's local buffers and ON COMMIT state
+ * if the prepared xact includes a DROP of a temp table.
+ *
+ * We must check this after executing any ON COMMIT actions, because
+ * they might still access a temp relation.
+ *
+ * However, since PostgreSQL 8.4, we do allow PREPARE TRANSATION if the
+ * transaction has accessed a temporary table with ON COMMIT DELETE ROWS
+ * action, as long as the transaction hasn't created or dropped one.  We
+ * work around the lock problem by releasing the locks early, in the PREPARE
+ * phase. That doesn't violate the two-phase locking protocol (not to be
+ * confused with two-phase commit!), as the lock would be released at the
+ * 2nd phase commit or rollback anyway, and the transaction won't acquire
+ * any more locks after PREPARE.
+ *
+ * This function is called by the lock manager in the PREPARE phase, to
+ * determine if the given relation is an ON COMMIT DELETE ROWS temporary
+ * table, and the lock manager should drop locks early.  Returns 'true', if
+ * it is, and 'false' if the table is not a temporary table. If the table
+ * is a temporary table, but it's not safe for PREPARE TRANSACTION, an error
+ * is thrown.
+ *
+ * The reason why we only do that for ON COMMIT DELETE ROWS tables, is that
+ * VACUUM expects that all xids in the table are finished. XXX That's not
+ * strictly true, it can deal with them but prints messages to log. Is there
+ * any other code that could be confused by that?
+ *
+ * XXX In principle this could be relaxed to allow some other useful special
+ * cases, such as a temp table created and dropped all within the
+ * transaction.  We would need to at least drop the local buffers, however.
+ */
+bool
+check_prepare_safe_temp_table(Oid relid)
+{
+	OnCommitItem *oc;
+
+	if (on_commits == NULL)
+		return false; /* No temp tables */
+
+	oc = (OnCommitItem *) hash_search(on_commits, &relid, HASH_FIND, NULL);
+
+	if (oc == NULL)
+		return false; /* Not a temp table */
+
+	/*
+	 * Allow access to ON COMMIT DELETE ROWS temporary tables,
+	 * that have not been created or dropped in this transaction.
+	 */
+	if (oc->oncommit == ONCOMMIT_DELETE_ROWS &&
+		oc->creating_subid == InvalidSubTransactionId &&
+		oc->deleting_subid == InvalidSubTransactionId)
 	{
-		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
+		return true;
+	}
+	else
+	{
+		/* Give appropriate error message */
+		if (oc->creating_subid != InvalidSubTransactionId ||
+			oc->deleting_subid != InvalidSubTransactionId)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot PREPARE a transaction that has created or dropped a temporary table")));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot PREPARE a transaction that has accessed a temporary table not defined with ON COMMIT DELETE ROWS")));
 
-		if (oc->relid == relid)
-		{
-			oc->deleting_subid = GetCurrentSubTransactionId();
-			break;
-		}
+		return false; /* keep compiler quite */
 	}
 }
 
@@ -7628,19 +7713,24 @@ remove_on_commit_action(Oid relid)
 void
 PreCommit_on_commit_actions(void)
 {
-	ListCell   *l;
+	HASH_SEQ_STATUS seq_status;
 	List	   *oids_to_truncate = NIL;
+	OnCommitItem *oc;
 
-	foreach(l, on_commits)
-	{
-		OnCommitItem *oc = (OnCommitItem *) lfirst(l);
-
-		/* Ignore entry if already dropped in this xact */
-		if (oc->deleting_subid != InvalidSubTransactionId)
-			continue;
+	if (on_commits == NULL)
+		return;
 
-		switch (oc->oncommit)
+	hash_seq_init(&seq_status, on_commits);
+	PG_TRY();
+	{
+		while ((oc = hash_seq_search(&seq_status)) != NULL)
 		{
+			/* Ignore entry if already dropped in this xact */
+			if (oc->deleting_subid != InvalidSubTransactionId)
+				continue;
+
+			switch (oc->oncommit)
+			{
 			case ONCOMMIT_NOOP:
 			case ONCOMMIT_PRESERVE_ROWS:
 				/* Do nothing (there shouldn't be such entries, actually) */
@@ -7665,8 +7755,15 @@ PreCommit_on_commit_actions(void)
 					Assert(oc->deleting_subid != InvalidSubTransactionId);
 					break;
 				}
+			}
 		}
 	}
+	PG_CATCH();
+	{
+		hash_seq_term(&seq_status);
+	}
+	PG_END_TRY();
+
 	if (oids_to_truncate != NIL)
 	{
 		heap_truncate(oids_to_truncate);
@@ -7685,36 +7782,36 @@ PreCommit_on_commit_actions(void)
 void
 AtEOXact_on_commit_actions(bool isCommit)
 {
-	ListCell   *cur_item;
-	ListCell   *prev_item;
+	HASH_SEQ_STATUS seq_status;
+	OnCommitItem *oc;
 
-	prev_item = NULL;
-	cur_item = list_head(on_commits);
+	if (on_commits == NULL)
+		return;
 
-	while (cur_item != NULL)
+	hash_seq_init(&seq_status, on_commits);
+	PG_TRY();
 	{
-		OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
-
-		if (isCommit ? oc->deleting_subid != InvalidSubTransactionId :
-			oc->creating_subid != InvalidSubTransactionId)
+		while ((oc = hash_seq_search(&seq_status)) != NULL)
 		{
-			/* cur_item must be removed */
-			on_commits = list_delete_cell(on_commits, cur_item, prev_item);
-			pfree(oc);
-			if (prev_item)
-				cur_item = lnext(prev_item);
+			if (isCommit ? oc->deleting_subid != InvalidSubTransactionId :
+				oc->creating_subid != InvalidSubTransactionId)
+			{
+				/* current item must be removed */
+				hash_search(on_commits, &oc->relid, HASH_REMOVE, NULL);
+			}
 			else
-				cur_item = list_head(on_commits);
-		}
-		else
-		{
-			/* cur_item must be preserved */
-			oc->creating_subid = InvalidSubTransactionId;
-			oc->deleting_subid = InvalidSubTransactionId;
-			prev_item = cur_item;
-			cur_item = lnext(prev_item);
+			{
+				/* current item must be preserved */
+				oc->creating_subid = InvalidSubTransactionId;
+				oc->deleting_subid = InvalidSubTransactionId;
+			}
 		}
 	}
+	PG_CATCH();
+	{
+		hash_seq_term(&seq_status);
+	}
+	PG_END_TRY();
 }
 
 /*
@@ -7728,35 +7825,35 @@ void
 AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid,
 							  SubTransactionId parentSubid)
 {
-	ListCell   *cur_item;
-	ListCell   *prev_item;
+	HASH_SEQ_STATUS seq_status;
+	OnCommitItem *oc;
 
-	prev_item = NULL;
-	cur_item = list_head(on_commits);
+	if (on_commits == NULL)
+		return;
 
-	while (cur_item != NULL)
+	hash_seq_init(&seq_status, on_commits);
+	PG_TRY();
 	{
-		OnCommitItem *oc = (OnCommitItem *) lfirst(cur_item);
-
-		if (!isCommit && oc->creating_subid == mySubid)
+		while ((oc = hash_seq_search(&seq_status)) != NULL)
 		{
-			/* cur_item must be removed */
-			on_commits = list_delete_cell(on_commits, cur_item, prev_item);
-			pfree(oc);
-			if (prev_item)
-				cur_item = lnext(prev_item);
+			if (!isCommit && oc->creating_subid == mySubid)
+			{
+				/* current item must be removed */
+				hash_search(on_commits, &oc->relid, HASH_REMOVE, NULL);
+			}
 			else
-				cur_item = list_head(on_commits);
-		}
-		else
-		{
-			/* cur_item must be preserved */
-			if (oc->creating_subid == mySubid)
-				oc->creating_subid = parentSubid;
-			if (oc->deleting_subid == mySubid)
-				oc->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
-			prev_item = cur_item;
-			cur_item = lnext(prev_item);
+			{
+				/* current item must be preserved */
+				if (oc->creating_subid == mySubid)
+					oc->creating_subid = parentSubid;
+				if (oc->deleting_subid == mySubid)
+					oc->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId;
+			}
 		}
 	}
+	PG_CATCH();
+	{
+		hash_seq_term(&seq_status);
+	}
+	PG_END_TRY();
 }
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index ccc6562..eb7d70f 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -35,6 +35,7 @@
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/twophase_rmgr.h"
+#include "commands/tablecmds.h"
 #include "miscadmin.h"
 #include "pg_trace.h"
 #include "pgstat.h"
@@ -1863,6 +1864,17 @@ AtPrepare_Locks(void)
 		if (locallock->nLocks <= 0)
 			continue;
 
+		/*
+		 * Locks on ON COMMIT DELETE ROWS temp tables are released in
+		 * PREPARE TRANSACTION phase
+		 */
+		if (locallock->tag.lock.locktag_type == LOCKTAG_RELATION)
+		{
+			Oid relid = locallock->tag.lock.locktag_field2;
+			if (check_prepare_safe_temp_table(relid))
+				continue;
+		}
+
 		/* Scan to verify there are no session locks */
 		for (i = locallock->numLockOwners - 1; i >= 0; i--)
 		{
@@ -1944,6 +1956,17 @@ PostPrepare_Locks(TransactionId xid)
 		if (locallock->tag.lock.locktag_type == LOCKTAG_VIRTUALTRANSACTION)
 			continue;
 
+		/*
+		 * Locks on ON COMMIT DELETE ROWS temp tables are released in
+		 * PREPARE TRANSACTION phase
+		 */
+		if (locallock->tag.lock.locktag_type == LOCKTAG_RELATION)
+		{
+			Oid relid = locallock->tag.lock.locktag_field2;
+			if (check_prepare_safe_temp_table(relid))
+				continue;
+		}
+
 		/* We already checked there are no session locks */
 
 		/* Mark the proclock to show we need to release this lockmode */
@@ -1993,6 +2016,17 @@ PostPrepare_Locks(TransactionId xid)
 			if (lock->tag.locktag_type == LOCKTAG_VIRTUALTRANSACTION)
 				goto next_item;
 
+			/*
+			 * Locks on ON COMMIT DELETE ROWS temp tables are released in
+			 * PREPARE TRANSACTION phase
+			 */
+			if (lock->tag.locktag_type == LOCKTAG_RELATION)
+			{
+				Oid relid = lock->tag.locktag_field2;
+				if (check_prepare_safe_temp_table(relid))
+					goto next_item;
+			}
+
 			PROCLOCK_PRINT("PostPrepare_Locks", proclock);
 			LOCK_PRINT("PostPrepare_Locks", lock, 0);
 			Assert(lock->nRequested >= 0);
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index dad6ef8..5fca0d6 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -44,9 +44,6 @@ extern bool XactReadOnly;
 /* Asynchronous commits */
 extern bool XactSyncCommit;
 
-/* Kluge for 2PC support */
-extern bool MyXactAccessedTempRel;
-
 /*
  *	start- and end-of-transaction callbacks for dynamically loaded modules
  */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 5a43d27..81dd51c 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -63,6 +63,7 @@ extern void change_varattnos_of_a_node(Node *node, const AttrNumber *newattno);
 
 extern void register_on_commit_action(Oid relid, OnCommitAction action);
 extern void remove_on_commit_action(Oid relid);
+extern bool check_prepare_safe_temp_table(Oid relid);
 
 extern void PreCommit_on_commit_actions(void);
 extern void AtEOXact_on_commit_actions(bool isCommit);
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to