From 2125d347fdfb3b4d373cdfe549e6c6c2471c6f17 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Thu, 15 Oct 2020 13:22:44 +1300
Subject: [PATCH] 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.

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 | 60 +++++++++++++++++++++++------------
 1 file changed, 40 insertions(+), 20 deletions(-)

diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 1d4aa482cc..14a1942f8a 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);
 
@@ -303,6 +331,10 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 		if (!RelFileNodeBackendIsTemp(rnode))
 			register_forget_request(rnode, forkNum, 0 /* first seg */ );
 
+		/* Prevent other backends' fds from holding on to the disk space */
+		if (!RelFileNodeBackendIsTemp(rnode))
+			do_truncate(path);
+
 		/* Next unlink the file */
 		ret = unlink(path);
 		if (ret < 0 && errno != ENOENT)
@@ -312,25 +344,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 */ );
@@ -358,6 +373,11 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
 				register_forget_request(rnode, forkNum, segno);
 
 			sprintf(segpath, "%s.%u", path, segno);
+
+			/* Prevent other backends' fds from holding on to the disk space */
+			if (!RelFileNodeBackendIsTemp(rnode))
+				do_truncate(segpath);
+
 			if (unlink(segpath) < 0)
 			{
 				/* ENOENT is expected after the last segment... */
-- 
2.20.1

