summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2011-08-16 13:12:23 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2011-08-16 13:12:23 -0400
commit8407a11c5a004671142a9ac2fe29de3844360b0b (patch)
tree20dda460bf0bd7e152802d42f50c0dc8ba2e0c49
parentd52555576887cb408b11589a2c07d5b2d06f892d (diff)
Fix race condition in relcache init file invalidation.
The previous code tried to synchronize by unlinking the init file twice, but that doesn't actually work: it leaves a window wherein a third process could read the already-stale init file but miss the SI messages that would tell it the data is stale. The result would be bizarre failures in catalog accesses, typically "could not read block 0 in file ..." later during startup. Instead, hold RelCacheInitLock across both the unlink and the sending of the SI messages. This is more straightforward, and might even be a bit faster since only one unlink call is needed. This has been wrong since it was put in (in 2002!), so back-patch to all supported releases.
-rw-r--r--src/backend/utils/cache/inval.c8
-rw-r--r--src/backend/utils/cache/relcache.c68
-rw-r--r--src/include/utils/relcache.h3
3 files changed, 43 insertions, 36 deletions
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index e87b3641352..c5b55407dbc 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -795,10 +795,10 @@ inval_twophase_postcommit(TransactionId xid, uint16 info,
SendSharedInvalidMessage(msg);
break;
case TWOPHASE_INFO_FILE_BEFORE:
- RelationCacheInitFileInvalidate(true);
+ RelationCacheInitFilePreInvalidate();
break;
case TWOPHASE_INFO_FILE_AFTER:
- RelationCacheInitFileInvalidate(false);
+ RelationCacheInitFilePostInvalidate();
break;
default:
Assert(false);
@@ -845,7 +845,7 @@ AtEOXact_Inval(bool isCommit)
* unless we committed.
*/
if (transInvalInfo->RelcacheInitFileInval)
- RelationCacheInitFileInvalidate(true);
+ RelationCacheInitFilePreInvalidate();
AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,
&transInvalInfo->CurrentCmdInvalidMsgs);
@@ -854,7 +854,7 @@ AtEOXact_Inval(bool isCommit)
SendSharedInvalidMessage);
if (transInvalInfo->RelcacheInitFileInval)
- RelationCacheInitFileInvalidate(false);
+ RelationCacheInitFilePostInvalidate();
}
else if (transInvalInfo != NULL)
{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2c36024c20c..c28db129381 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -3947,8 +3947,8 @@ write_relcache_init_file(void)
* updated by SI message processing, but we can't be sure whether what we
* wrote out was up-to-date.)
*
- * This mustn't run concurrently with RelationCacheInitFileInvalidate, so
- * grab a serialization lock for the duration.
+ * This mustn't run concurrently with the code that unlinks an init file
+ * and sends SI messages, so grab a serialization lock for the duration.
*/
LWLockAcquire(RelCacheInitLock, LW_EXCLUSIVE);
@@ -4012,49 +4012,55 @@ RelationIdIsInInitFile(Oid relationId)
* changed one or more of the relation cache entries that are kept in the
* init file.
*
- * We actually need to remove the init file twice: once just before sending
- * the SI messages that include relcache inval for such relations, and once
- * just after sending them. The unlink before ensures that a backend that's
- * currently starting cannot read the now-obsolete init file and then miss
- * the SI messages that will force it to update its relcache entries. (This
- * works because the backend startup sequence gets into the PGPROC array before
- * trying to load the init file.) The unlink after is to synchronize with a
- * backend that may currently be trying to write an init file based on data
- * that we've just rendered invalid. Such a backend will see the SI messages,
- * but we can't leave the init file sitting around to fool later backends.
- *
- * Ignore any failure to unlink the file, since it might not be there if
- * no backend has been started since the last removal.
+ * To be safe against concurrent inspection or rewriting of the init file,
+ * we must take RelCacheInitLock, then remove the old init file, then send
+ * the SI messages that include relcache inval for such relations, and then
+ * release RelCacheInitLock. This serializes the whole affair against
+ * write_relcache_init_file, so that we can be sure that any other process
+ * that's concurrently trying to create a new init file won't move an
+ * already-stale version into place after we unlink. Also, because we unlink
+ * before sending the SI messages, a backend that's currently starting cannot
+ * read the now-obsolete init file and then miss the SI messages that will
+ * force it to update its relcache entries. (This works because the backend
+ * startup sequence gets into the sinval array before trying to load the init
+ * file.)
+ *
+ * We take the lock and do the unlink in RelationCacheInitFilePreInvalidate,
+ * then release the lock in RelationCacheInitFilePostInvalidate. Caller must
+ * send any pending SI messages between those calls.
*/
void
-RelationCacheInitFileInvalidate(bool beforeSend)
+RelationCacheInitFilePreInvalidate(void)
{
char initfilename[MAXPGPATH];
snprintf(initfilename, sizeof(initfilename), "%s/%s",
DatabasePath, RELCACHE_INIT_FILENAME);
- if (beforeSend)
- {
- /* no interlock needed here */
- unlink(initfilename);
- }
- else
+ LWLockAcquire(RelCacheInitLock, LW_EXCLUSIVE);
+
+ if (unlink(initfilename) < 0)
{
/*
- * We need to interlock this against write_relcache_init_file, to
- * guard against possibility that someone renames a new-but-
- * already-obsolete init file into place just after we unlink. With
- * the interlock, it's certain that write_relcache_init_file will
- * notice our SI inval message before renaming into place, or else
- * that we will execute second and successfully unlink the file.
+ * The file might not be there if no backend has been started since
+ * the last removal. But complain about failures other than ENOENT.
+ * Fortunately, it's not too late to abort the transaction if we
+ * can't get rid of the would-be-obsolete init file.
*/
- LWLockAcquire(RelCacheInitLock, LW_EXCLUSIVE);
- unlink(initfilename);
- LWLockRelease(RelCacheInitLock);
+ if (errno != ENOENT)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not remove cache file \"%s\": %m",
+ initfilename)));
}
}
+void
+RelationCacheInitFilePostInvalidate(void)
+{
+ LWLockRelease(RelCacheInitLock);
+}
+
/*
* Remove the init file for a given database during postmaster startup.
*
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index c5005abce51..0a4d9bf86ff 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -71,7 +71,8 @@ extern void RelationCacheMarkNewRelfilenode(Relation rel);
* Routines to help manage rebuilding of relcache init file
*/
extern bool RelationIdIsInInitFile(Oid relationId);
-extern void RelationCacheInitFileInvalidate(bool beforeSend);
+extern void RelationCacheInitFilePreInvalidate(void);
+extern void RelationCacheInitFilePostInvalidate(void);
extern void RelationCacheInitFileRemove(const char *dbPath);
/* should be used only by relcache.c and catcache.c */