From e9d611fb3f45cdd9641fe6e73bb4fa7f7f08aad2 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Tue, 10 Nov 2020 13:39:00 +0400
Subject: [PATCH v2] Free disk space for dropped relations on commit.

When committing a transaction that dropped a relation, we previously
truncated only the first segment file to free up disk space, because it
won't be unlinked until the next checkpoint.

Truncate later segments too, even though we unlink them on commit.  This
frees the disk space immediately, even if other backends have open file
descriptors and might take a long time to get around to handling shared
invalidation events and closing them.

Also extend the same behavior to the first segment in recovery.

Author: Thomas Munro
Bug: #16663
Reported-by: Denis Patron <denis.patron@previnet.it>
Discussion: https://postgr.es/m/16663-fe97ccf9932fc800%40postgresql.org
---
 src/backend/storage/smgr/md.c | 74 +++++++++++++++++++++++------------
 1 file changed, 48 insertions(+), 26 deletions(-)

diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 1d4aa482cc..785ee7ef1e 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -286,11 +286,39 @@ mdunlink(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 		mdunlinkfork(rnode, forkNum, isRedo);
 }
 
+/*
+ * Reduce the size of a file to zero, to free up disk space.
+ */
+static void
+do_truncate(const char *path)
+{
+	int			ret;
+	int			fd;
+
+	/* truncate(2) would be easier here, but Windows hasn't got it */
+	fd = OpenTransientFile(path, O_RDWR | PG_BINARY);
+	if (fd >= 0)
+	{
+		int			save_errno;
+
+		ret = ftruncate(fd, 0);
+		save_errno = errno;
+		CloseTransientFile(fd);
+		errno = save_errno;
+	}
+	else
+		ret = -1;
+	if (ret < 0 && errno != ENOENT)
+		ereport(WARNING,
+				(errcode_for_file_access(),
+				 errmsg("could not truncate file \"%s\": %m", path)));
+}
+
 static void
 mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 {
 	char	   *path;
-	int			ret;
+	int			ret = 0;
 
 	path = relpath(rnode, forkNum);
 
@@ -299,10 +327,15 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 	 */
 	if (isRedo || forkNum != MAIN_FORKNUM || RelFileNodeBackendIsTemp(rnode))
 	{
-		/* First, forget any pending sync requests for the first segment */
 		if (!RelFileNodeBackendIsTemp(rnode))
+		{
+			/* First, forget any pending sync requests for the first segment */
 			register_forget_request(rnode, forkNum, 0 /* first seg */ );
 
+			/* Prevent other backends' fds from holding on to the disk space */
+			do_truncate(path);
+		}
+
 		/* Next unlink the file */
 		ret = unlink(path);
 		if (ret < 0 && errno != ENOENT)
@@ -312,25 +345,8 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 	}
 	else
 	{
-		/* truncate(2) would be easier here, but Windows hasn't got it */
-		int			fd;
-
-		fd = OpenTransientFile(path, O_RDWR | PG_BINARY);
-		if (fd >= 0)
-		{
-			int			save_errno;
-
-			ret = ftruncate(fd, 0);
-			save_errno = errno;
-			CloseTransientFile(fd);
-			errno = save_errno;
-		}
-		else
-			ret = -1;
-		if (ret < 0 && errno != ENOENT)
-			ereport(WARNING,
-					(errcode_for_file_access(),
-					 errmsg("could not truncate file \"%s\": %m", path)));
+		/* Prevent other backends' fds from holding on to the disk space */
+		do_truncate(path);
 
 		/* Register request to unlink first segment later */
 		register_unlink_segment(rnode, forkNum, 0 /* first seg */ );
@@ -350,14 +366,20 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 		 */
 		for (segno = 1;; segno++)
 		{
-			/*
-			 * Forget any pending sync requests for this segment before we try
-			 * to unlink.
-			 */
+			sprintf(segpath, "%s.%u", path, segno);
+
 			if (!RelFileNodeBackendIsTemp(rnode))
+			{
+				/*
+				 * Forget any pending sync requests for this segment before we try
+				 * to unlink.
+				 */
 				register_forget_request(rnode, forkNum, segno);
 
-			sprintf(segpath, "%s.%u", path, segno);
+				/* Prevent other backends' fds from holding on to the disk space */
+				do_truncate(segpath);
+			}
+
 			if (unlink(segpath) < 0)
 			{
 				/* ENOENT is expected after the last segment... */
-- 
2.28.0

