agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feedOrphaned Files in PostgreSQL
12+ messages / 5 participants
[nested] [flat]
* Orphaned Files in PostgreSQL
@ 2025-02-18 10:46 Ashutosh Sharma <ashu.coek88@gmail.com>
0 siblings, 2 replies; 12+ messages in thread
From: Ashutosh Sharma @ 2025-02-18 10:46 UTC (permalink / raw)
To: pgsql-hackers
Hi All,
While investigating one of our customer issues, we discovered several
orphaned data files on the disk that do not have corresponding entries
in the pg_class table. Upon further analysis, we identified specific
scenarios in PostgreSQL where this issue can occur. One such scenario
is as follows:
Consider a situation where a table is being created within a
transaction, and data is being loaded into it. If PostgreSQL
unexpectedly crashes while the transaction is still in progress, an
orphaned file may be left behind on the disk. In cases where multiple
such transactions occur, this can lead to the accumulation of numerous
orphaned files, resulting in significant disk space consumption.
Unfortunately, these files are not cleared during PostgreSQL's restart
process.
We have discussed this issue internally, and one proposed solution
involves adding a marker file to the disk for any table created within
a transaction, immediately upon its creation. This marker file would
then be removed during the commit process. If the transaction is
aborted due to a server crash, the marker file and the corresponding
disk file would be cleared at the end of the recovery process during
server startup.
I would appreciate your thoughts on this solution. Should you have any
suggestions or alternative approaches, I would be grateful to hear
them.
Additionally, I am unsure if this issue has already been reported or
if it is currently being addressed. If that is the case, I would be
grateful if you could point me to the relevant discussion thread so I
can follow the progress and contribute if needed.
Thank you for your time and assistance.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2025-02-18 11:17 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Ashutosh Sharma <ashu.coek88@gmail.com>
1 sibling, 0 replies; 12+ messages in thread
From: Bertrand Drouvot @ 2025-02-18 11:17 UTC (permalink / raw)
To: Ashutosh Sharma <ashu.coek88@gmail.com>; +Cc: pgsql-hackers
Hi,
On Tue, Feb 18, 2025 at 04:16:02PM +0530, Ashutosh Sharma wrote:
> Additionally, I am unsure if this issue has already been reported or
> if it is currently being addressed. If that is the case, I would be
> grateful if you could point me to the relevant discussion thread so I
> can follow the progress and contribute if needed.
I think that it is a known issue (see [1]). The thread links to a discussion that
could provide a fix (If I understood correctly) and introduces a way/extension to
clean those orphaned files in a clean way (using a dirty snapshot). Idea was to
gauge if there is interest to add this extension in contrib (would certainly
need some polishing and code work to meet the "contrib" expectations though).
[1]: https://www.postgresql.org/message-id/flat/7ff08868-843a-c39c-c96d-7e7f77fe5f5c%40amazon.com
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-08-20 09:10 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Ashutosh Sharma <ashu.coek88@gmail.com>
1 sibling, 1 reply; 12+ messages in thread
From: Ashutosh Sharma @ 2026-08-20 09:10 UTC (permalink / raw)
To: pgsql-hackers; Andres Freund <andres@anarazel.de>; Drouvot, Bertrand <bertranddrouvot.pg@gmail.com>
Hi All,
I am revisiting this thread to propose a possible fix for this known issue.
Issue:
=====
When PostgreSQL undergoes an unclean shutdown while a transaction is
creating a relation and loading a large amount of data into it, the
transaction is considered aborted during recovery. However, the
relation files created by that transaction can remain in the data
directory, occupying disk space without any visible catalog entry.
This causes several problems:
1) It can fill up the disk space and take the server down.
2) It can increase backup size and backup duration.
3) It adds unnecessary file system scanning, syncing, and maintenance overhead.
4) It forces manual work to tell real orphaned files apart from valid ones.
5) It wastes disk space until an admin finds and removes the files.
At present, when a transaction creates WAL-logged relation storage,
PostgreSQL adds it to a backend-local "pending delete" list, so the
files get cleaned up if the transaction aborts normally. The problem
is that this list only lives in memory. If the server crashes
mid-transaction, that list is lost and since WAL recovery replays
actions forward (redo) rather than undoing them, there may be no abort
record left to trigger cleanup of those files.
Proposed solution:
==============
To address this, I propose maintaining a durable relation-creation
marker for every transactionally created permanent relation.
The marker would be created under a new pg_relcreate directory and
store the following information:
1) The complete RelFileLocator.
2) The XID of the transaction that created the storage.
3) A marker format version.
4) A magic value identifying the file type.
5) A CRC protecting the marker contents.
typedef struct RelationCreateMarker
{
uint32 magic;
uint32 version;
RelFileLocator rlocator;
TransactionId xid;
pg_crc32c crc;
} RelationCreateMarker;
The marker filename would be derived from the tablespace OID, database
OID, and relfilenumber. For example it would be something like:
<tblspc_oid>_<dbid>_<relfilenode>. This marker is created, used, and
removed at different points in a relation's lifecycle - creation,
commit, abort, and recovery. Let's look at how each step would work.
A) Relation creation would proceed in this order:
----------------------------------------------------------------
For every persistent relation that needs storage, would go through
following steps inside RelationCreateStorage():
1) Insert an XLOG_SMGR_CREATE record marked as requiring a creation marker.
2) Create and fsync the marker file.
3) Create the physical relation file.
4) Register the existing delete-on-abort pending-delete entry.
This ordering guarantees that relation storage cannot become durable
without either a durable marker or WAL capable of reconstructing that
marker.
B) WAL replay:
--------------------
The XLOG_SMGR_CREATE payload would include a flag indicating whether a
marker is required. The creating XID remains in the common WAL record
header.
During redo, smgr_redo() would:
1) Obtain the XID from the WAL record header.
2) Recreate or validate the marker.
3) Recreate the relation fork as it does today.
If an identical marker already exists, redo accepts it. A conflicting
or corrupt marker may cause the recovery to fail rather than overwrite
unresolved cleanup state.
C) Normal commit:
-------------------------
For a committing transaction:
1) Force the transaction's commit WAL record to local durable storage.
2) Keep the relation.
3) Remove the marker durably when processing the "pending-delete" list.
Forcing synchronous commit is important here. Otherwise, PostgreSQL
could remove the marker and then crash before the asynchronous commit
record reaches disk. Recovery would subsequently consider the
transaction aborted, but no marker would remain to identify its
relation files.
The end result of the successful commit is as follows:
relation file: present
catalog row: committed
marker: removed
One subtle but important case: if the server crashes after the commit
record is durable but before the marker gets removed, that's fine.
When PostgreSQL restarts, its recovery process sees that this
transaction committed, keeps the table file, and simply cleans up the
now-unnecessary leftover marker.
D) Normal abort:
----------------------
For a transaction that aborted:
The existing pending-delete mechanism remains responsible for deleting
relation storage. It calls mdunlink() which truncates the main-fork to
make it a tombstone file and lets the next checkpoint remove the
tombstoned file and the marker file. The subsequent checkpoint unlinks
both the relation file and the marker file.
The end result is:
relation file: removed
catalog row: aborted / invisible
marker: removed
E) End-of-recovery cleanup:
-------------------------------------
After WAL replay and prepared-transaction recovery have completed,
PostgreSQL scans pg_relcreate.
For every valid marker:
1) If the creating XID committed, retain the relation and remove the
stale marker.
2) If the XID belongs to a prepared transaction, retain both the
relation and marker.
3) Otherwise, treat the transaction as crash-aborted and remove all
relation forks and the marker.
If PostgreSQL crashes after deleting the relation but before deleting
the marker, the next recovery attempts the relation deletion again and
then removes the marker.
F) Prepared transactions:
----------------------------------
Markers belonging to prepared transactions must survive recovery.
1) COMMIT PREPARED retains the relation and removes its marker after
the commit record is durable.
2) ROLLBACK PREPARED deletes the relation through the existing
two-phase pending-delete information, with marker removal following
physical tombstone cleanup.
G) Subtransactions:
---------------------------
1) On a subtransaction commit, its pending-delete entry transfers to
the parent transaction, so the marker remains until the top-level
transaction finishes.
2) On subtransaction abort, relation deletion starts immediately, but
the marker is retained until checkpoint processing removes the
relation tombstone.
Performance considerations:
---------------------------------------
The principal cost is additional I/O for transactional permanent
relation creation:
1) Writing and fsyncing a small marker file.
2) Fsyncing the marker directory.
3) Forcing local synchronous commit for transactions that created
marked storage.
This affects operations that create new permanent relfilenumbers, such
as CREATE TABLE, CREATE INDEX, REINDEX, VACUUM FULL, CLUSTER, and some
relation rewrites. Ordinary DML does not incur this cost.
The solution described above is implemented in the attached patch,
please take a look and share your feedback.
--
With Regards,
Ashutosh Sharma.
On Tue, Feb 18, 2025 at 4:16 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
>
> Hi All,
>
> While investigating one of our customer issues, we discovered several
> orphaned data files on the disk that do not have corresponding entries
> in the pg_class table. Upon further analysis, we identified specific
> scenarios in PostgreSQL where this issue can occur. One such scenario
> is as follows:
>
> Consider a situation where a table is being created within a
> transaction, and data is being loaded into it. If PostgreSQL
> unexpectedly crashes while the transaction is still in progress, an
> orphaned file may be left behind on the disk. In cases where multiple
> such transactions occur, this can lead to the accumulation of numerous
> orphaned files, resulting in significant disk space consumption.
> Unfortunately, these files are not cleared during PostgreSQL's restart
> process.
>
> We have discussed this issue internally, and one proposed solution
> involves adding a marker file to the disk for any table created within
> a transaction, immediately upon its creation. This marker file would
> then be removed during the commit process. If the transaction is
> aborted due to a server crash, the marker file and the corresponding
> disk file would be cleared at the end of the recovery process during
> server startup.
>
> I would appreciate your thoughts on this solution. Should you have any
> suggestions or alternative approaches, I would be grateful to hear
> them.
>
> Additionally, I am unsure if this issue has already been reported or
> if it is currently being addressed. If that is the case, I would be
> grateful if you could point me to the relevant discussion thread so I
> can follow the progress and contribute if needed.
>
> Thank you for your time and assistance.
>
> --
> With Regards,
> Ashutosh Sharma.
Attachments:
[application/octet-stream] 0001-Remove-files-left-by-crash-aborted-relation-creation.patch (22.1K, ../../CAE9k0Pnd3fAGGJjxRPv3CqH1AiRB-1Y=4_Ah-XV9hC7ZZQXK_w@mail.gmail.com/2-0001-Remove-files-left-by-crash-aborted-relation-creation.patch)
download | inline diff:
From cb67895a2227a19774a9093e76c16957bf8b746b Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <ashu.coek88@gmail.com>
Date: Wed, 19 Aug 2026 11:41:37 +0000
Subject: [PATCH] Remove files left by crash-aborted relation creation
PostgreSQL tracks files created by a transaction in backend-local
pending-delete state. An immediate shutdown loses that state. Files
created by transactions that recovery treats as aborted can remain on
disk without a visible catalog entry.
Introduce durable creation markers in pg_relcreate for transactional
permanent relation storage. Store the full RelFileLocator and creator
XID, format version, and CRC in each marker. Write and fsync each
marker and its directory before creating the physical relation file.
Mark SMGR_CREATE WAL records that require a marker, allowing redo to
recreate the marker idempotently before recreating relation storage.
Force a local synchronous commit for transactions that create marked
storage, then remove their markers only after commit WAL is durable.
For aborts, retain the marker until the checkpointer removes the SMGR
unlink tombstone. This prevents stale cleanup state from deleting a
new relation that reuses the same relfilenumber.
At the end of recovery, reconcile markers after prepared transactions
have been restored. Keep files belonging to committed or prepared
transactions, and remove storage belonging to crash-aborted ones.
Handle COMMIT PREPARED by removing its delete-on-abort markers.
Teach initdb about pg_relcreate, document the directory, and expose
marker creation in WAL descriptions. Add recovery tests covering
normal commit, crash abort across a checkpoint, and prepared commit.
---
doc/src/sgml/storage.sgml | 6 +
src/backend/access/rmgrdesc/smgrdesc.c | 2 +
src/backend/access/transam/twophase.c | 32 +++
src/backend/access/transam/xlog.c | 4 +
src/backend/catalog/storage.c | 225 +++++++++++++++++-
src/backend/storage/smgr/md.c | 12 +-
src/bin/initdb/initdb.c | 1 +
src/include/access/twophase.h | 1 +
src/include/catalog/storage.h | 2 +
src/include/catalog/storage_xlog.h | 3 +
src/test/recovery/meson.build | 1 +
.../recovery/t/056_relation_create_markers.pl | 76 ++++++
12 files changed, 362 insertions(+), 3 deletions(-)
create mode 100644 src/test/recovery/t/056_relation_create_markers.pl
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 19924b98d71..b0a10b14238 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -106,6 +106,12 @@ Item
<entry>Subdirectory containing replication slot data</entry>
</row>
+<row>
+ <entry><filename>pg_relcreate</filename></entry>
+ <entry>Subdirectory containing durable markers for transactional relation
+ creation</entry>
+</row>
+
<row>
<entry><filename>pg_serial</filename></entry>
<entry>Subdirectory containing information about committed serializable transactions</entry>
diff --git a/src/backend/access/rmgrdesc/smgrdesc.c b/src/backend/access/rmgrdesc/smgrdesc.c
index aaf1b07999d..f20c88099d6 100644
--- a/src/backend/access/rmgrdesc/smgrdesc.c
+++ b/src/backend/access/rmgrdesc/smgrdesc.c
@@ -29,6 +29,8 @@ smgr_desc(StringInfo buf, XLogReaderState *record)
appendStringInfoString(buf,
relpathperm(xlrec->rlocator, xlrec->forkNum).str);
+ if (xlrec->createMarker)
+ appendStringInfoString(buf, " with creation marker");
}
else if (info == XLOG_SMGR_TRUNCATE)
{
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 48e478a4ecb..4ea721e7439 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -1498,6 +1498,32 @@ StandbyTransactionIdIsPrepared(TransactionId xid)
return result;
}
+bool
+TwoPhaseTransactionIdIsPrepared(TransactionId xid)
+{
+ TransactionId topxid;
+ bool result = false;
+
+ Assert(TransactionIdIsValid(xid));
+ topxid = SubTransGetTopmostTransaction(xid);
+
+ LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
+ for (int i = 0; i < TwoPhaseState->numPrepXacts; i++)
+ {
+ GlobalTransaction gxact = TwoPhaseState->prepXacts[i];
+
+ if (gxact->valid &&
+ TransactionIdEquals(XidFromFullTransactionId(gxact->fxid), topxid))
+ {
+ result = true;
+ break;
+ }
+ }
+ LWLockRelease(TwoPhaseStateLock);
+
+ return result;
+}
+
/*
* FinishPreparedTransaction: execute COMMIT PREPARED or ROLLBACK PREPARED
*/
@@ -1625,6 +1651,12 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
/* Make sure files supposed to be dropped are dropped */
DropRelationFiles(delrels, ndelrels, false);
+ if (isCommit)
+ {
+ for (int i = 0; i < hdr->nabortrels; i++)
+ RelationCreateMarkerCleanup(&abortrels[i]);
+ }
+
if (isCommit)
pgstat_execute_transactional_drops(hdr->ncommitstats, commitstats, false);
else
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index de4c96e135f..f0cb0ff5200 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -67,6 +67,7 @@
#include "catalog/catversion.h"
#include "catalog/pg_control.h"
#include "catalog/pg_database.h"
+#include "catalog/storage.h"
#include "common/controldata_utils.h"
#include "common/file_utils.h"
#include "executor/instrument.h"
@@ -6715,6 +6716,9 @@ StartupXLOG(void)
if (standbyState != STANDBY_DISABLED)
ShutdownRecoveryTransactionEnvironment();
+ if (performedWalRecovery)
+ RelationCreateMarkerCleanupAtEndOfRecovery();
+
/*
* If there were cascading standby servers connected to us, nudge any wal
* sender processes to notice that we've been promoted.
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..a9c40cf2092 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -20,6 +20,8 @@
#include "postgres.h"
#include "access/visibilitymap.h"
+#include "access/transam.h"
+#include "access/twophase.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
@@ -28,7 +30,9 @@
#include "catalog/storage_xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
+#include "port/pg_crc32c.h"
#include "storage/bulk_write.h"
+#include "storage/fd.h"
#include "storage/freespace.h"
#include "storage/proc.h"
#include "storage/smgr.h"
@@ -39,6 +43,179 @@
/* GUC variables */
int wal_skip_threshold = 2048; /* in kilobytes */
+#define RELATION_CREATE_MARKER_DIR "pg_relcreate"
+#define RELATION_CREATE_MARKER_MAGIC 0x52434D4B
+#define RELATION_CREATE_MARKER_VERSION 1
+
+typedef struct RelationCreateMarker
+{
+ uint32 magic;
+ uint32 version;
+ RelFileLocator rlocator;
+ TransactionId xid;
+ pg_crc32c crc;
+} RelationCreateMarker;
+
+static void
+relation_create_marker_path(char *path, Size size,
+ const RelFileLocator *rlocator)
+{
+ snprintf(path, size, RELATION_CREATE_MARKER_DIR "/%u_%u_%u",
+ rlocator->spcOid, rlocator->dbOid, rlocator->relNumber);
+}
+
+static void
+create_relation_create_marker(const RelFileLocator *rlocator,
+ TransactionId xid)
+{
+ RelationCreateMarker marker;
+ RelationCreateMarker existing;
+ char path[MAXPGPATH];
+ int fd;
+ int save_errno;
+
+ relation_create_marker_path(path, sizeof(path), rlocator);
+ marker.magic = RELATION_CREATE_MARKER_MAGIC;
+ marker.version = RELATION_CREATE_MARKER_VERSION;
+ marker.rlocator = *rlocator;
+ marker.xid = xid;
+ INIT_CRC32C(marker.crc);
+ COMP_CRC32C(marker.crc, &marker, offsetof(RelationCreateMarker, crc));
+ FIN_CRC32C(marker.crc);
+
+ fd = OpenTransientFile(path, O_WRONLY | O_CREAT | O_EXCL | PG_BINARY);
+ if (fd < 0 && errno == EEXIST)
+ {
+ fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);
+ if (fd >= 0 &&
+ read(fd, &existing, sizeof(existing)) == sizeof(existing) &&
+ existing.magic == marker.magic &&
+ existing.version == marker.version &&
+ RelFileLocatorEquals(existing.rlocator, marker.rlocator) &&
+ TransactionIdEquals(existing.xid, marker.xid) &&
+ EQ_CRC32C(existing.crc, marker.crc))
+ {
+ CloseTransientFile(fd);
+ return;
+ }
+ if (fd >= 0)
+ CloseTransientFile(fd);
+ errno = EEXIST;
+ }
+ if (fd < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not create relation creation marker \"%s\": %m",
+ path)));
+
+ if (write(fd, &marker, sizeof(marker)) != sizeof(marker) ||
+ pg_fsync(fd) != 0)
+ {
+ save_errno = errno;
+ CloseTransientFile(fd);
+ errno = save_errno;
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not write relation creation marker \"%s\": %m",
+ path)));
+ }
+
+ if (CloseTransientFile(fd) != 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not close relation creation marker \"%s\": %m",
+ path)));
+
+ fsync_fname(RELATION_CREATE_MARKER_DIR, true);
+}
+
+void
+RelationCreateMarkerCleanup(const RelFileLocator *rlocator)
+{
+ char path[MAXPGPATH];
+
+ relation_create_marker_path(path, sizeof(path), rlocator);
+ (void) durable_unlink(path, WARNING);
+}
+
+void
+RelationCreateMarkerCleanupAtEndOfRecovery(void)
+{
+ DIR *dir;
+ struct dirent *de;
+
+ dir = AllocateDir(RELATION_CREATE_MARKER_DIR);
+ if (dir == NULL)
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not open relation creation marker directory \"%s\": %m",
+ RELATION_CREATE_MARKER_DIR)));
+
+ while ((de = ReadDir(dir, RELATION_CREATE_MARKER_DIR)) != NULL)
+ {
+ RelationCreateMarker marker;
+ char path[MAXPGPATH];
+ char expected_path[MAXPGPATH];
+ int fd;
+ pg_crc32c crc;
+
+ if (de->d_name[0] == '.')
+ continue;
+
+ snprintf(path, sizeof(path), RELATION_CREATE_MARKER_DIR "/%s",
+ de->d_name);
+ fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);
+ if (fd < 0 || read(fd, &marker, sizeof(marker)) != sizeof(marker))
+ {
+ if (fd >= 0)
+ CloseTransientFile(fd);
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation marker \"%s\"", path)));
+ }
+
+ INIT_CRC32C(crc);
+ COMP_CRC32C(crc, &marker, offsetof(RelationCreateMarker, crc));
+ FIN_CRC32C(crc);
+ relation_create_marker_path(expected_path, sizeof(expected_path),
+ &marker.rlocator);
+ if (marker.magic != RELATION_CREATE_MARKER_MAGIC ||
+ marker.version != RELATION_CREATE_MARKER_VERSION ||
+ !TransactionIdIsValid(marker.xid) ||
+ !EQ_CRC32C(crc, marker.crc) || strcmp(path, expected_path) != 0)
+ {
+ CloseTransientFile(fd);
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation marker \"%s\"", path)));
+ }
+ if (CloseTransientFile(fd) != 0)
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not close relation creation marker \"%s\": %m",
+ path)));
+
+ if (TransactionIdDidCommit(marker.xid))
+ {
+ RelationCreateMarkerCleanup(&marker.rlocator);
+ continue;
+ }
+ if (TwoPhaseTransactionIdIsPrepared(marker.xid))
+ continue;
+
+ {
+ SMgrRelation srel = smgropen(marker.rlocator,
+ INVALID_PROC_NUMBER);
+
+ smgrdounlinkall(&srel, 1, true);
+ smgrclose(srel);
+ RelationCreateMarkerCleanup(&marker.rlocator);
+ }
+ }
+
+ FreeDir(dir);
+}
+
/*
* We keep a list of all relations (represented as RelFileLocator values)
* that have been created or deleted in the current transaction. When
@@ -63,6 +240,7 @@ typedef struct PendingRelDelete
{
RelFileLocator rlocator; /* relation that may need to be deleted */
ProcNumber procNumber; /* INVALID_PROC_NUMBER if not a temp rel */
+ TransactionId createXid; /* XID stored in the creation marker */
bool atCommit; /* T=delete at commit; F=delete at abort */
int nestLevel; /* xact nesting level of request */
struct PendingRelDelete *next; /* linked-list link */
@@ -124,6 +302,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
{
SMgrRelation srel;
ProcNumber procNumber;
+ TransactionId createXid = InvalidTransactionId;
bool needs_wal;
Assert(!IsInParallelMode()); /* couldn't update pendingSyncHash */
@@ -148,9 +327,18 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
}
srel = smgropen(rlocator, procNumber);
+
+ if (needs_wal && register_delete)
+ {
+ createXid = log_smgrcreate_marker(&srel->smgr_rlocator.locator,
+ MAIN_FORKNUM);
+ create_relation_create_marker(&rlocator, createXid);
+ ForceSyncCommit();
+ }
+
smgrcreate(srel, MAIN_FORKNUM, false);
- if (needs_wal)
+ if (needs_wal && !register_delete)
log_smgrcreate(&srel->smgr_rlocator.locator, MAIN_FORKNUM);
/*
@@ -165,6 +353,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->rlocator = rlocator;
pending->procNumber = procNumber;
+ pending->createXid = createXid;
pending->atCommit = false; /* delete if abort */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
@@ -186,17 +375,35 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
void
log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum)
{
- xl_smgr_create xlrec;
+ xl_smgr_create xlrec = {0};
/*
* Make an XLOG entry reporting the file creation.
*/
xlrec.rlocator = *rlocator;
xlrec.forkNum = forkNum;
+ xlrec.createMarker = false;
+
+ XLogBeginInsert();
+ XLogRegisterData(&xlrec, sizeof(xlrec));
+ XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE | XLR_SPECIAL_REL_UPDATE);
+}
+
+TransactionId
+log_smgrcreate_marker(const RelFileLocator *rlocator, ForkNumber forkNum)
+{
+ xl_smgr_create xlrec = {0};
+ TransactionId xid = GetCurrentTransactionId();
+
+ xlrec.rlocator = *rlocator;
+ xlrec.forkNum = forkNum;
+ xlrec.createMarker = true;
XLogBeginInsert();
XLogRegisterData(&xlrec, sizeof(xlrec));
XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE | XLR_SPECIAL_REL_UPDATE);
+
+ return xid;
}
/*
@@ -213,6 +420,7 @@ RelationDropStorage(Relation rel)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->rlocator = rel->rd_locator;
pending->procNumber = rel->rd_backend;
+ pending->createXid = InvalidTransactionId;
pending->atCommit = true; /* delete if commit */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
@@ -262,6 +470,9 @@ RelationPreserveStorage(RelFileLocator rlocator, bool atCommit)
if (RelFileLocatorEquals(rlocator, pending->rlocator)
&& pending->atCommit == atCommit)
{
+ if (!atCommit && TransactionIdIsValid(pending->createXid))
+ RelationCreateMarkerCleanup(&pending->rlocator);
+
/* unlink and delete list entry */
if (prev)
prev->next = next;
@@ -717,6 +928,8 @@ smgrDoPendingDeletes(bool isCommit)
srels[nrels++] = srel;
}
+ else if (isCommit && TransactionIdIsValid(pending->createXid))
+ RelationCreateMarkerCleanup(&pending->rlocator);
/* must explicitly free the list entry */
pfree(pending);
/* prev does not change */
@@ -990,6 +1203,14 @@ smgr_redo(XLogReaderState *record)
{
xl_smgr_create *xlrec = (xl_smgr_create *) XLogRecGetData(record);
SMgrRelation reln;
+ TransactionId xid = XLogRecGetXid(record);
+
+ if (xlrec->createMarker)
+ {
+ if (!TransactionIdIsValid(xid))
+ elog(PANIC, "relation creation marker WAL record has no transaction ID");
+ create_relation_create_marker(&xlrec->rlocator, xid);
+ }
reln = smgropen(xlrec->rlocator, INVALID_PROC_NUMBER);
smgrcreate(reln, xlrec->forkNum, true);
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 780c88c0630..b0ac91154a6 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -27,6 +27,7 @@
#include <sys/file.h>
#include "access/xlogutils.h"
+#include "catalog/storage.h"
#include "commands/tablespace.h"
#include "common/file_utils.h"
#include "miscadmin.h"
@@ -1957,6 +1958,8 @@ int
mdunlinkfiletag(const FileTag *ftag, char *path)
{
RelPathStr p;
+ int result;
+ int save_errno;
/* We only unlink tombstone files through this mechanism */
Assert(ftag->forknum == MAIN_FORKNUM && ftag->segno == 0);
@@ -1966,7 +1969,14 @@ mdunlinkfiletag(const FileTag *ftag, char *path)
strlcpy(path, p.str, MAXPGPATH);
/* Try to unlink the file. */
- return unlink(path);
+ result = unlink(path);
+ save_errno = errno;
+
+ if (result == 0 || save_errno == ENOENT)
+ RelationCreateMarkerCleanup(&ftag->rlocator);
+
+ errno = save_errno;
+ return result;
}
/*
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index b3d496372ad..f2ff108749c 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -236,6 +236,7 @@ static const char *const subdirs[] = {
"pg_commit_ts",
"pg_dynshmem",
"pg_notify",
+ "pg_relcreate",
"pg_serial",
"pg_snapshots",
"pg_subtrans",
diff --git a/src/include/access/twophase.h b/src/include/access/twophase.h
index 1d2ff42c9b7..152b1e0d2f6 100644
--- a/src/include/access/twophase.h
+++ b/src/include/access/twophase.h
@@ -48,6 +48,7 @@ extern GlobalTransaction MarkAsPreparing(FullTransactionId fxid, const char *gid
extern void StartPrepare(GlobalTransaction gxact);
extern void EndPrepare(GlobalTransaction gxact);
extern bool StandbyTransactionIdIsPrepared(TransactionId xid);
+extern bool TwoPhaseTransactionIdIsPrepared(TransactionId xid);
extern TransactionId PrescanPreparedTransactions(TransactionId **xids_p,
int *nxids_p);
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 70f619a6d6f..6cf7ca5762a 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -27,6 +27,8 @@ extern SMgrRelation RelationCreateStorage(RelFileLocator rlocator,
bool register_delete);
extern void RelationDropStorage(Relation rel);
extern void RelationPreserveStorage(RelFileLocator rlocator, bool atCommit);
+extern void RelationCreateMarkerCleanup(const RelFileLocator *rlocator);
+extern void RelationCreateMarkerCleanupAtEndOfRecovery(void);
extern void RelationPreTruncate(Relation rel);
extern void RelationTruncate(Relation rel, BlockNumber nblocks);
extern void RelationCopyStorage(SMgrRelation src, SMgrRelation dst,
diff --git a/src/include/catalog/storage_xlog.h b/src/include/catalog/storage_xlog.h
index c1b2f736669..4924f050313 100644
--- a/src/include/catalog/storage_xlog.h
+++ b/src/include/catalog/storage_xlog.h
@@ -34,6 +34,7 @@ typedef struct xl_smgr_create
{
RelFileLocator rlocator;
ForkNumber forkNum;
+ bool createMarker;
} xl_smgr_create;
/* flags for xl_smgr_truncate */
@@ -51,6 +52,8 @@ typedef struct xl_smgr_truncate
} xl_smgr_truncate;
extern void log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum);
+extern TransactionId log_smgrcreate_marker(const RelFileLocator *rlocator,
+ ForkNumber forkNum);
extern void smgr_redo(XLogReaderState *record);
extern void smgr_desc(StringInfo buf, XLogReaderState *record);
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 39ec8c4946d..b8e43847c2e 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -64,6 +64,7 @@ tests += {
't/053_standby_login_event_trigger.pl',
't/054_unlogged_sequence_promotion.pl',
't/055_cascade_reconnect.pl',
+ 't/056_relation_create_markers.pl',
],
},
}
diff --git a/src/test/recovery/t/056_relation_create_markers.pl b/src/test/recovery/t/056_relation_create_markers.pl
new file mode 100644
index 00000000000..a1b1ef74c8c
--- /dev/null
+++ b/src/test/recovery/t/056_relation_create_markers.pl
@@ -0,0 +1,76 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test cleanup of permanent relation files created by transactions that are
+# still in progress when the server crashes.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('relation_create_markers');
+$node->init();
+$node->append_conf('postgresql.conf', 'max_prepared_transactions = 10');
+$node->start();
+
+my $marker_dir = $node->data_dir . '/pg_relcreate';
+
+$node->safe_psql('postgres', 'CREATE TABLE committed_relation (a int)');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 0, 'committed relation leaves no creation marker');
+
+my $session = $node->background_psql('postgres');
+$session->query_safe('BEGIN');
+my $relation_path = $session->query_safe(
+ 'CREATE TABLE crash_aborted_relation (a int); '
+ . q{SELECT pg_relation_filepath('crash_aborted_relation')});
+
+ok(-f $node->data_dir . '/' . $relation_path,
+ 'uncommitted relation file exists before crash');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 1, 'uncommitted relation has a creation marker');
+
+# Move the redo pointer past the creation record. Recovery therefore needs
+# the persistent marker; replay-local tracking of the create record is not
+# sufficient.
+$node->safe_psql('postgres', 'CHECKPOINT');
+$node->stop('immediate');
+$node->start();
+
+is($node->safe_psql('postgres',
+ q{SELECT to_regclass('crash_aborted_relation') IS NULL}),
+ 't', 'crash-aborted relation is absent from the catalog');
+ok(!-e $node->data_dir . '/' . $relation_path,
+ 'crash-aborted relation file is removed during recovery');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 0, 'processed creation marker is removed');
+
+my $prepared_path = $node->safe_psql(
+ 'postgres',
+ q{BEGIN;
+CREATE TABLE prepared_relation (a int);
+SELECT pg_relation_filepath('prepared_relation');
+PREPARE TRANSACTION 'relation_create_marker';});
+ok(-f $node->data_dir . '/' . $prepared_path,
+ 'prepared relation file exists');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 1, 'prepared relation retains its creation marker');
+
+$node->stop('immediate');
+$node->start();
+
+ok(-f $node->data_dir . '/' . $prepared_path,
+ 'prepared relation file survives recovery');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 1, 'recovery retains prepared relation marker');
+$node->safe_psql('postgres',
+ q{COMMIT PREPARED 'relation_create_marker'});
+is($node->safe_psql('postgres',
+ q{SELECT to_regclass('prepared_relation') IS NOT NULL}),
+ 't', 'committed prepared relation is visible');
+is(scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($marker_dir)),
+ 0, 'commit prepared removes relation marker');
+
+$node->stop();
+done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-08-21 06:55 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Ashutosh Sharma <ashu.coek88@gmail.com>
0 siblings, 2 replies; 12+ messages in thread
From: Bertrand Drouvot @ 2026-08-21 06:55 UTC (permalink / raw)
To: Ashutosh Sharma <ashu.coek88@gmail.com>; +Cc: pgsql-hackers; Andres Freund <andres@anarazel.de>
Hi,
On Thu, Aug 20, 2026 at 02:40:25PM +0530, Ashutosh Sharma wrote:
> Hi All,
>
> I am revisiting this thread to propose a possible fix for this known issue.
Thanks for working on this!
> This ordering guarantees that relation storage cannot become durable
> without either a durable marker or WAL capable of reconstructing that
> marker.
I wonder if logging XLOG_SMGR_CREATE before smgrcreate() could interact badly
with a concurrent checkpoint? I looked at [1] and it looks like it used a separate
PRECREATE record and kept the usual CREATE record after physical creation.
Could this be reused here, independently of the rest of its undo infrastructure?
[1]: https://postgr.es/m/CAEepm%3D0ULqYgM2aFeOnrx6YrtBg3xUdxALoyCG%2BXpssKqmezug%40mail.gmail.com
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-08-21 09:51 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 0 replies; 12+ messages in thread
From: Ashutosh Sharma @ 2026-08-21 09:51 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers; Andres Freund <andres@anarazel.de>
Hi,
On Fri, Aug 21, 2026 at 12:25 PM Bertrand Drouvot
<bertranddrouvot.pg@gmail.com> wrote:
>
> Hi,
>
> On Thu, Aug 20, 2026 at 02:40:25PM +0530, Ashutosh Sharma wrote:
> > Hi All,
> >
> > I am revisiting this thread to propose a possible fix for this known issue.
>
> Thanks for working on this!
>
> > This ordering guarantees that relation storage cannot become durable
> > without either a durable marker or WAL capable of reconstructing that
> > marker.
>
> I wonder if logging XLOG_SMGR_CREATE before smgrcreate() could interact badly
> with a concurrent checkpoint? I looked at [1] and it looks like it used a separate
> PRECREATE record and kept the usual CREATE record after physical creation.
>
Thanks, that's a valid concern. Logging XLOG_SMGR_CREATE before
smgrcreate() would break the established ordering, and could allow a
concurrent checkpoint's redo pointer to advance past the create record
before the physical file has actually been created and registered for
synchronization.
The separate PRECREATE approach in [1] looks like it could be reused
independently of its undo infrastructure. PRECREATE could represent
just the intent to create the durable marker, while the existing
CREATE record would retain its normal position after physical file
creation. I'll explore this possibility further and incorporate it
into the next version of the patch.
> Could this be reused here, independently of the rest of its undo infrastructure?
>
> [1]: https://postgr.es/m/CAEepm%3D0ULqYgM2aFeOnrx6YrtBg3xUdxALoyCG%2BXpssKqmezug%40mail.gmail.com
>
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-23 11:18 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 1 reply; 12+ messages in thread
From: Ashutosh Sharma @ 2026-09-23 11:18 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers; Andres Freund <andres@anarazel.de>
Hi,
On Fri, Aug 21, 2026 at 12:25 PM Bertrand Drouvot
<bertranddrouvot.pg@gmail.com> wrote:
>
> Hi,
>
> On Thu, Aug 20, 2026 at 02:40:25PM +0530, Ashutosh Sharma wrote:
> > Hi All,
> >
> > I am revisiting this thread to propose a possible fix for this known issue.
>
> Thanks for working on this!
>
> > This ordering guarantees that relation storage cannot become durable
> > without either a durable marker or WAL capable of reconstructing that
> > marker.
>
> I wonder if logging XLOG_SMGR_CREATE before smgrcreate() could interact badly
> with a concurrent checkpoint? I looked at [1] and it looks like it used a separate
> PRECREATE record and kept the usual CREATE record after physical creation.
>
This has been addressed in the attached patch.
The patch also replaces the earlier design of creating one marker file
per relation file with a manifest file per transaction XID. Each
manifest records the durable relations created by that transaction.
On transaction completion, the corresponding manifests are retired and
removed. If PostgreSQL crashes before transaction cleanup completes,
recovery examines the remaining manifests after WAL replay. Manifests
belonging to committed or prepared transactions are preserved or
cleaned up appropriately, while relation files recorded for aborted or
incomplete transactions are removed. The processed manifests are then
removed.
The patch also handles subtransactions, prepared transactions, standby
replay, truncated manifests, and failures that leave retired manifests
behind.
Please take a look and let me know.
--
With Regards,
Ashutosh Sharma.
Attachments:
[application/octet-stream] v2-0001-Remove-relation-files-left-by-crash-aborted-transact.patch (40.4K, ../../CAE9k0P=BcbT-Z5m+qy-0qhf92CEtTs6nYg5OE=9oLJOAvz2yrg@mail.gmail.com/2-v2-0001-Remove-relation-files-left-by-crash-aborted-transact.patch)
download | inline diff:
From ae8e1116951f86cac5fd88aea11231848626c8b8 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <ashu.coek88@gmail.com>
Date: Mon, 21 Sep 2026 09:42:01 +0000
Subject: [PATCH] Remove relation files left by crash-aborted transactions
A crash can occur after permanent relation storage is created but
before transaction abort cleanup removes it. Such files are not
represented in the catalogs after recovery, so they can remain on disk
indefinitely.
Track transactional relation creation in durable, append-only
manifests under pg_relcreate, with one manifest per creating XID.
WAL-log PRECREATE before creating storage so standbys can reconstruct
the manifest. WAL-log PRESERVE when storage is intentionally removed
from delete-on-abort processing. Include a CRC in each record and
repair a torn final record before appending.
Mark transaction completion records that own manifests so commit and
abort replay can remove manifests for the complete transaction tree.
Retain manifests across PREPARE TRANSACTION and handle both prepared
outcomes. Reconcile remaining manifests at checkpoints and at the end
of recovery. Keep committed and prepared relations, while removing
storage from aborted or incomplete transactions.
Add recovery tests for multiple relations sharing one manifest,
crash abort, active checkpoints, prepared commit and rollback, and
streaming standby commit and abort replay.
---
doc/src/sgml/storage.sgml | 6 +
src/backend/access/rmgrdesc/smgrdesc.c | 23 +-
src/backend/access/transam/twophase.c | 40 +-
src/backend/access/transam/xact.c | 12 +
src/backend/access/transam/xlog.c | 6 +
src/backend/catalog/storage.c | 506 +++++++++++++++++-
src/backend/storage/smgr/md.c | 9 +-
src/bin/initdb/initdb.c | 1 +
src/bin/pg_rewind/parsexlog.c | 16 +
src/include/access/twophase.h | 1 +
src/include/access/xact.h | 4 +
src/include/catalog/storage.h | 6 +
src/include/catalog/storage_xlog.h | 15 +
src/test/recovery/meson.build | 1 +
.../recovery/t/057_relation_create_markers.pl | 187 +++++++
15 files changed, 826 insertions(+), 7 deletions(-)
create mode 100644 src/test/recovery/t/057_relation_create_markers.pl
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 83de016eaa5..6fc5df9c94b 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -106,6 +106,12 @@ Item
<entry>Subdirectory containing replication slot data</entry>
</row>
+<row>
+ <entry><filename>pg_relcreate</filename></entry>
+ <entry>Subdirectory containing durable manifests for transactional relation
+ creation</entry>
+</row>
+
<row>
<entry><filename>pg_serial</filename></entry>
<entry>Subdirectory containing information about committed serializable transactions</entry>
diff --git a/src/backend/access/rmgrdesc/smgrdesc.c b/src/backend/access/rmgrdesc/smgrdesc.c
index aaf1b07999d..698a0e05fce 100644
--- a/src/backend/access/rmgrdesc/smgrdesc.c
+++ b/src/backend/access/rmgrdesc/smgrdesc.c
@@ -23,7 +23,22 @@ smgr_desc(StringInfo buf, XLogReaderState *record)
char *rec = XLogRecGetData(record);
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
- if (info == XLOG_SMGR_CREATE)
+ if (info == XLOG_SMGR_PRECREATE)
+ {
+ xl_smgr_precreate *xlrec = (xl_smgr_precreate *) rec;
+
+ appendStringInfoString(buf,
+ relpathperm(xlrec->rlocator, MAIN_FORKNUM).str);
+ }
+ else if (info == XLOG_SMGR_PRESERVE)
+ {
+ xl_smgr_preserve *xlrec = (xl_smgr_preserve *) rec;
+
+ appendStringInfo(buf, "%s xid %u",
+ relpathperm(xlrec->rlocator, MAIN_FORKNUM).str,
+ xlrec->xid);
+ }
+ else if (info == XLOG_SMGR_CREATE)
{
xl_smgr_create *xlrec = (xl_smgr_create *) rec;
@@ -47,6 +62,12 @@ smgr_identify(uint8 info)
switch (info & ~XLR_INFO_MASK)
{
+ case XLOG_SMGR_PRECREATE:
+ id = "PRECREATE";
+ break;
+ case XLOG_SMGR_PRESERVE:
+ id = "PRESERVE";
+ break;
case XLOG_SMGR_CREATE:
id = "CREATE";
break;
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 48e478a4ecb..ef6ba903ff6 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -219,6 +219,7 @@ static void RecordTransactionCommitPrepared(TransactionId xid,
int ninvalmsgs,
SharedInvalidationMessage *invalmsgs,
bool initfileinval,
+ bool hasrelationcreate,
const char *gid);
static void RecordTransactionAbortPrepared(TransactionId xid,
int nchildren,
@@ -1498,6 +1499,32 @@ StandbyTransactionIdIsPrepared(TransactionId xid)
return result;
}
+bool
+TwoPhaseTransactionIdIsPrepared(TransactionId xid)
+{
+ TransactionId topxid;
+ bool result = false;
+
+ Assert(TransactionIdIsValid(xid));
+ topxid = SubTransGetTopmostTransaction(xid);
+
+ LWLockAcquire(TwoPhaseStateLock, LW_SHARED);
+ for (int i = 0; i < TwoPhaseState->numPrepXacts; i++)
+ {
+ GlobalTransaction gxact = TwoPhaseState->prepXacts[i];
+
+ if (gxact->valid &&
+ TransactionIdEquals(XidFromFullTransactionId(gxact->fxid), topxid))
+ {
+ result = true;
+ break;
+ }
+ }
+ LWLockRelease(TwoPhaseStateLock);
+
+ return result;
+}
+
/*
* FinishPreparedTransaction: execute COMMIT PREPARED or ROLLBACK PREPARED
*/
@@ -1583,7 +1610,8 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
hdr->ncommitstats,
commitstats,
hdr->ninvalmsgs, invalmsgs,
- hdr->initfileinval, gid);
+ hdr->initfileinval,
+ hdr->nabortrels > 0, gid);
else
RecordTransactionAbortPrepared(xid,
hdr->nsubxacts, children,
@@ -1625,6 +1653,9 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
/* Make sure files supposed to be dropped are dropped */
DropRelationFiles(delrels, ndelrels, false);
+ if (hdr->nabortrels > 0)
+ RelationCreateManifestCleanupTree(xid, hdr->nsubxacts, children);
+
if (isCommit)
pgstat_execute_transactional_drops(hdr->ncommitstats, commitstats, false);
else
@@ -2328,6 +2359,7 @@ RecordTransactionCommitPrepared(TransactionId xid,
int ninvalmsgs,
SharedInvalidationMessage *invalmsgs,
bool initfileinval,
+ bool hasrelationcreate,
const char *gid)
{
XLogRecPtr recptr;
@@ -2378,7 +2410,8 @@ RecordTransactionCommitPrepared(TransactionId xid,
nstats, stats,
ninvalmsgs, invalmsgs,
initfileinval,
- MyXactFlags | XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK,
+ MyXactFlags | XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK |
+ (hasrelationcreate ? XACT_FLAGS_HAS_RELATION_CREATE : 0),
xid, gid);
@@ -2475,7 +2508,8 @@ RecordTransactionAbortPrepared(TransactionId xid,
nchildren, children,
nrels, rels,
nstats, stats,
- MyXactFlags | XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK,
+ MyXactFlags | XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK |
+ (nrels > 0 ? XACT_FLAGS_HAS_RELATION_CREATE : 0),
xid, gid);
if (replorigin)
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index ebb010853cf..c5772f2018c 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -5913,6 +5913,8 @@ XactLogCommitRecord(TimestampTz commit_time,
xl_xinfo.xinfo |= XACT_COMPLETION_FORCE_SYNC_COMMIT;
if ((xactflags & XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK))
xl_xinfo.xinfo |= XACT_XINFO_HAS_AE_LOCKS;
+ if ((xactflags & XACT_FLAGS_HAS_RELATION_CREATE))
+ xl_xinfo.xinfo |= XACT_XINFO_HAS_RELATION_CREATE;
/*
* Check if the caller would like to ask standbys for immediate feedback
@@ -6080,6 +6082,8 @@ XactLogAbortRecord(TimestampTz abort_time,
if ((xactflags & XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK))
xl_xinfo.xinfo |= XACT_XINFO_HAS_AE_LOCKS;
+ if ((xactflags & XACT_FLAGS_HAS_RELATION_CREATE))
+ xl_xinfo.xinfo |= XACT_XINFO_HAS_RELATION_CREATE;
if (nsubxacts > 0)
{
@@ -6331,6 +6335,10 @@ xact_redo_commit(xl_xact_parsed_commit *parsed,
*/
if (XactCompletionApplyFeedback(parsed->xinfo))
XLogRequestWalReceiverReply();
+
+ if (parsed->xinfo & XACT_XINFO_HAS_RELATION_CREATE)
+ RelationCreateManifestCleanupTree(xid, parsed->nsubxacts,
+ parsed->subxacts);
}
/*
@@ -6419,6 +6427,10 @@ xact_redo_abort(xl_xact_parsed_abort *parsed, TransactionId xid,
pgstat_execute_transactional_drops(parsed->nstats, parsed->stats, true);
}
+
+ if (parsed->xinfo & XACT_XINFO_HAS_RELATION_CREATE)
+ RelationCreateManifestCleanupTree(xid, parsed->nsubxacts,
+ parsed->subxacts);
}
void
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9ec0be77ca0..c31089fde40 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -67,6 +67,7 @@
#include "catalog/catversion.h"
#include "catalog/pg_control.h"
#include "catalog/pg_database.h"
+#include "catalog/storage.h"
#include "common/controldata_utils.h"
#include "common/file_utils.h"
#include "executor/instrument.h"
@@ -6948,6 +6949,9 @@ StartupXLOG(void)
if (standbyState != STANDBY_DISABLED)
ShutdownRecoveryTransactionEnvironment();
+ if (performedWalRecovery)
+ RelationCreateManifestCleanupAtEndOfRecovery();
+
/*
* If there were cascading standby servers connected to us, nudge any wal
* sender processes to notice that we've been promoted.
@@ -8403,6 +8407,8 @@ CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
TRACE_POSTGRESQL_BUFFER_CHECKPOINT_SYNC_START();
CheckpointStats.ckpt_sync_t = GetCurrentTimestamp();
ProcessSyncRequests();
+ if (!RecoveryInProgress())
+ RelationCreateManifestCleanupAtCheckpoint();
CheckpointStats.ckpt_sync_end_t = GetCurrentTimestamp();
TRACE_POSTGRESQL_BUFFER_CHECKPOINT_DONE();
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..6f18d6c129b 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -20,6 +20,8 @@
#include "postgres.h"
#include "access/visibilitymap.h"
+#include "access/transam.h"
+#include "access/twophase.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
@@ -28,9 +30,12 @@
#include "catalog/storage_xlog.h"
#include "miscadmin.h"
#include "pgstat.h"
+#include "port/pg_crc32c.h"
#include "storage/bulk_write.h"
+#include "storage/fd.h"
#include "storage/freespace.h"
#include "storage/proc.h"
+#include "storage/procarray.h"
#include "storage/smgr.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
@@ -39,6 +44,387 @@
/* GUC variables */
int wal_skip_threshold = 2048; /* in kilobytes */
+#define RELATION_CREATE_MANIFEST_DIR "pg_relcreate"
+#define RELATION_CREATE_MANIFEST_MAGIC 0x52434D46
+#define RELATION_CREATE_MANIFEST_VERSION 1
+
+typedef enum RelationCreateManifestOperation
+{
+ RELATION_CREATE_MANIFEST_CREATE = 1,
+ RELATION_CREATE_MANIFEST_PRESERVE,
+ RELATION_CREATE_MANIFEST_COMPLETE
+} RelationCreateManifestOperation;
+
+typedef struct RelationCreateManifestRecord
+{
+ uint32 magic;
+ uint32 version;
+ uint32 operation;
+ TransactionId xid;
+ RelFileLocator rlocator;
+ pg_crc32c crc;
+} RelationCreateManifestRecord;
+
+static void
+relation_create_manifest_path(char *path, Size size, TransactionId xid)
+{
+ snprintf(path, size, RELATION_CREATE_MANIFEST_DIR "/%u", xid);
+}
+
+static bool
+append_relation_create_manifest(TransactionId xid,
+ const RelFileLocator *rlocator,
+ RelationCreateManifestOperation operation,
+ bool create_if_missing, int elevel)
+{
+ RelationCreateManifestRecord record = {0};
+ RelationCreateManifestRecord existing;
+ char path[MAXPGPATH];
+ int fd;
+ int save_errno;
+ off_t file_size;
+ off_t offset;
+ bool created = false;
+
+ relation_create_manifest_path(path, sizeof(path), xid);
+ record.magic = RELATION_CREATE_MANIFEST_MAGIC;
+ record.version = RELATION_CREATE_MANIFEST_VERSION;
+ record.operation = operation;
+ record.xid = xid;
+ if (rlocator != NULL)
+ record.rlocator = *rlocator;
+ INIT_CRC32C(record.crc);
+ COMP_CRC32C(record.crc, &record,
+ offsetof(RelationCreateManifestRecord, crc));
+ FIN_CRC32C(record.crc);
+
+ if (create_if_missing)
+ {
+ fd = OpenTransientFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
+ if (fd >= 0)
+ created = true;
+ else if (errno == EEXIST)
+ fd = OpenTransientFile(path, O_RDWR | PG_BINARY);
+ }
+ else
+ fd = OpenTransientFile(path, O_RDWR | PG_BINARY);
+ if (fd < 0)
+ {
+ if (!create_if_missing && errno == ENOENT)
+ return false;
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("could not open relation creation manifest \"%s\": %m",
+ path)));
+ }
+
+ file_size = lseek(fd, 0, SEEK_END);
+ if (file_size < 0)
+ {
+ save_errno = errno;
+ CloseTransientFile(fd);
+ errno = save_errno;
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("could not seek relation creation manifest \"%s\": %m",
+ path)));
+ }
+ offset = file_size - file_size % sizeof(existing);
+
+ if (offset > 0)
+ {
+ pg_crc32c crc;
+ ssize_t nread;
+
+ nread = pg_pread(fd, &existing, sizeof(existing),
+ (pgoff_t) (offset - sizeof(existing)));
+ if (nread != sizeof(existing))
+ {
+ save_errno = nread < 0 ? errno : EIO;
+ CloseTransientFile(fd);
+ errno = save_errno;
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("could not read relation creation manifest \"%s\": %m",
+ path)));
+ }
+ INIT_CRC32C(crc);
+ COMP_CRC32C(crc, &existing,
+ offsetof(RelationCreateManifestRecord, crc));
+ FIN_CRC32C(crc);
+ if (existing.magic != RELATION_CREATE_MANIFEST_MAGIC ||
+ existing.version != RELATION_CREATE_MANIFEST_VERSION ||
+ !TransactionIdEquals(existing.xid, xid) ||
+ (existing.operation != RELATION_CREATE_MANIFEST_CREATE &&
+ existing.operation != RELATION_CREATE_MANIFEST_PRESERVE &&
+ existing.operation != RELATION_CREATE_MANIFEST_COMPLETE) ||
+ !EQ_CRC32C(crc, existing.crc))
+ {
+ CloseTransientFile(fd);
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation manifest \"%s\"", path)));
+ }
+ if (file_size == offset && existing.operation == operation &&
+ RelFileLocatorEquals(existing.rlocator, record.rlocator))
+ {
+ CloseTransientFile(fd);
+ return true;
+ }
+ }
+
+ if (ftruncate(fd, offset) != 0 || lseek(fd, offset, SEEK_SET) < 0 ||
+ write(fd, &record, sizeof(record)) != sizeof(record) ||
+ pg_fsync(fd) != 0)
+ {
+ save_errno = errno;
+ CloseTransientFile(fd);
+ errno = save_errno;
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("could not write relation creation manifest \"%s\": %m",
+ path)));
+ }
+
+ if (CloseTransientFile(fd) != 0)
+ ereport(elevel,
+ (errcode_for_file_access(),
+ errmsg("could not close relation creation manifest \"%s\": %m",
+ path)));
+
+ if (created)
+ fsync_fname(RELATION_CREATE_MANIFEST_DIR, true);
+
+ return true;
+}
+
+void
+RelationCreateManifestCleanup(TransactionId xid)
+{
+ char path[MAXPGPATH];
+
+ /*
+ * Make a failed or non-durable unlink harmless. This is done only after
+ * transaction-end storage cleanup, so a durable COMPLETE record proves
+ * that this manifest must never drive recovery-time relation removal.
+ */
+ if (!append_relation_create_manifest(xid, NULL,
+ RELATION_CREATE_MANIFEST_COMPLETE,
+ false, PANIC))
+ return;
+
+ relation_create_manifest_path(path, sizeof(path), xid);
+ if (unlink(path) == 0)
+ fsync_fname(RELATION_CREATE_MANIFEST_DIR, true);
+ else if (errno != ENOENT)
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not remove relation creation manifest \"%s\": %m",
+ path)));
+}
+
+void
+RelationCreateManifestCleanupTree(TransactionId xid, int nsubxacts,
+ TransactionId *subxacts)
+{
+ RelationCreateManifestCleanup(xid);
+ for (int i = 0; i < nsubxacts; i++)
+ RelationCreateManifestCleanup(subxacts[i]);
+}
+
+static void
+relation_create_manifest_reconcile(bool end_of_recovery)
+{
+ DIR *dir;
+ struct dirent *de;
+
+ dir = AllocateDir(RELATION_CREATE_MANIFEST_DIR);
+ if (dir == NULL)
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not open relation creation manifest directory \"%s\": %m",
+ RELATION_CREATE_MANIFEST_DIR)));
+
+ while ((de = ReadDir(dir, RELATION_CREATE_MANIFEST_DIR)) != NULL)
+ {
+ RelationCreateManifestRecord *records = NULL;
+ RelationCreateManifestRecord record;
+ char path[MAXPGPATH];
+ char *endptr;
+ unsigned long parsed_xid;
+ TransactionId xid;
+ int fd;
+ int nrecords = 0;
+ int maxrecords = 0;
+ ssize_t nread;
+ bool complete = false;
+
+ if (de->d_name[0] == '.')
+ continue;
+
+ errno = 0;
+ parsed_xid = strtoul(de->d_name, &endptr, 10);
+ if (errno != 0 || *endptr != '\0' || parsed_xid > PG_UINT32_MAX ||
+ !TransactionIdIsValid((TransactionId) parsed_xid))
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation manifest name \"%s\"",
+ de->d_name)));
+ xid = (TransactionId) parsed_xid;
+ snprintf(path, sizeof(path), RELATION_CREATE_MANIFEST_DIR "/%s",
+ de->d_name);
+ fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);
+ if (fd < 0)
+ {
+ if (errno == ENOENT)
+ continue;
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not open relation creation manifest \"%s\": %m",
+ path)));
+ }
+
+ while ((nread = read(fd, &record, sizeof(record))) == sizeof(record))
+ {
+ pg_crc32c crc;
+
+ INIT_CRC32C(crc);
+ COMP_CRC32C(crc, &record,
+ offsetof(RelationCreateManifestRecord, crc));
+ FIN_CRC32C(crc);
+ if (record.magic != RELATION_CREATE_MANIFEST_MAGIC ||
+ record.version != RELATION_CREATE_MANIFEST_VERSION ||
+ !TransactionIdEquals(record.xid, xid) ||
+ (record.operation != RELATION_CREATE_MANIFEST_CREATE &&
+ record.operation != RELATION_CREATE_MANIFEST_PRESERVE &&
+ record.operation != RELATION_CREATE_MANIFEST_COMPLETE) ||
+ !EQ_CRC32C(crc, record.crc))
+ {
+ CloseTransientFile(fd);
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation manifest \"%s\"", path)));
+ }
+ if (complete)
+ {
+ CloseTransientFile(fd);
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("invalid relation creation manifest \"%s\"", path)));
+ }
+ complete = record.operation == RELATION_CREATE_MANIFEST_COMPLETE;
+ if (nrecords == maxrecords)
+ {
+ if (maxrecords == 0)
+ {
+ maxrecords = 8;
+ records = palloc_array(RelationCreateManifestRecord,
+ maxrecords);
+ }
+ else
+ {
+ maxrecords *= 2;
+ records = repalloc_array(records,
+ RelationCreateManifestRecord,
+ maxrecords);
+ }
+ }
+ records[nrecords++] = record;
+ }
+ if (nread != 0)
+ {
+ int save_errno = nread < 0 ? errno : EIO;
+
+ CloseTransientFile(fd);
+ errno = save_errno;
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not read relation creation manifest \"%s\": %m",
+ path)));
+ }
+ if (CloseTransientFile(fd) != 0)
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not close relation creation manifest \"%s\": %m",
+ path)));
+
+ if (complete)
+ {
+ RelationCreateManifestCleanup(xid);
+ pfree(records);
+ continue;
+ }
+
+ if (!end_of_recovery && TransactionIdIsInProgress(xid))
+ {
+ pfree(records);
+ continue;
+ }
+ if (TwoPhaseTransactionIdIsPrepared(xid))
+ {
+ pfree(records);
+ continue;
+ }
+ if (TransactionIdDidCommit(xid))
+ {
+ RelationCreateManifestCleanup(xid);
+ pfree(records);
+ continue;
+ }
+ if (!end_of_recovery)
+ {
+ /*
+ * Normal abort processing owns storage cleanup while backends are
+ * running. Racing it here could unlink a newly reused relfilenumber.
+ */
+ pfree(records);
+ continue;
+ }
+
+ for (int i = 0; i < nrecords; i++)
+ {
+ bool preserved = false;
+
+ if (records[i].operation != RELATION_CREATE_MANIFEST_CREATE)
+ continue;
+ for (int j = i + 1; j < nrecords; j++)
+ {
+ if (records[j].operation == RELATION_CREATE_MANIFEST_PRESERVE &&
+ RelFileLocatorEquals(records[j].rlocator,
+ records[i].rlocator))
+ {
+ preserved = true;
+ break;
+ }
+ }
+ if (!preserved)
+ {
+ SMgrRelation srel = smgropen(records[i].rlocator,
+ INVALID_PROC_NUMBER);
+
+ smgrdounlinkall(&srel, 1, true);
+ smgrclose(srel);
+ }
+ }
+ RelationCreateManifestCleanup(xid);
+ pfree(records);
+ }
+
+ FreeDir(dir);
+}
+
+void
+RelationCreateManifestCleanupAtCheckpoint(void)
+{
+ relation_create_manifest_reconcile(false);
+}
+
+void
+RelationCreateManifestCleanupAtEndOfRecovery(void)
+{
+ relation_create_manifest_reconcile(true);
+}
+
/*
* We keep a list of all relations (represented as RelFileLocator values)
* that have been created or deleted in the current transaction. When
@@ -63,6 +449,7 @@ typedef struct PendingRelDelete
{
RelFileLocator rlocator; /* relation that may need to be deleted */
ProcNumber procNumber; /* INVALID_PROC_NUMBER if not a temp rel */
+ TransactionId createXid; /* XID owning the creation manifest */
bool atCommit; /* T=delete at commit; F=delete at abort */
int nestLevel; /* xact nesting level of request */
struct PendingRelDelete *next; /* linked-list link */
@@ -124,6 +511,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
{
SMgrRelation srel;
ProcNumber procNumber;
+ TransactionId createXid = InvalidTransactionId;
bool needs_wal;
Assert(!IsInParallelMode()); /* couldn't update pendingSyncHash */
@@ -148,6 +536,17 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
}
srel = smgropen(rlocator, procNumber);
+
+ if (needs_wal && register_delete)
+ {
+ createXid = log_smgrprecreate(&srel->smgr_rlocator.locator);
+ append_relation_create_manifest(createXid, &rlocator,
+ RELATION_CREATE_MANIFEST_CREATE,
+ true, ERROR);
+ MyXactFlags |= XACT_FLAGS_HAS_RELATION_CREATE;
+ ForceSyncCommit();
+ }
+
smgrcreate(srel, MAIN_FORKNUM, false);
if (needs_wal)
@@ -165,6 +564,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->rlocator = rlocator;
pending->procNumber = procNumber;
+ pending->createXid = createXid;
pending->atCommit = false; /* delete if abort */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
@@ -186,7 +586,7 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
void
log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum)
{
- xl_smgr_create xlrec;
+ xl_smgr_create xlrec = {0};
/*
* Make an XLOG entry reporting the file creation.
@@ -199,6 +599,40 @@ log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum)
XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE | XLR_SPECIAL_REL_UPDATE);
}
+/*
+ * Log the intent to create a relation before its durable marker is written.
+ */
+TransactionId
+log_smgrprecreate(const RelFileLocator *rlocator)
+{
+ xl_smgr_precreate xlrec;
+ TransactionId xid = GetCurrentTransactionId();
+
+ xlrec.rlocator = *rlocator;
+
+ XLogBeginInsert();
+ XLogRegisterData(&xlrec, sizeof(xlrec));
+ XLogInsert(RM_SMGR_ID, XLOG_SMGR_PRECREATE | XLR_SPECIAL_REL_UPDATE);
+
+ return xid;
+}
+
+/*
+ * Log that a relation is no longer to be removed if its creator aborts.
+ */
+void
+log_smgrpreserve(const RelFileLocator *rlocator, TransactionId xid)
+{
+ xl_smgr_preserve xlrec;
+
+ xlrec.rlocator = *rlocator;
+ xlrec.xid = xid;
+
+ XLogBeginInsert();
+ XLogRegisterData(&xlrec, sizeof(xlrec));
+ XLogInsert(RM_SMGR_ID, XLOG_SMGR_PRESERVE | XLR_SPECIAL_REL_UPDATE);
+}
+
/*
* RelationDropStorage
* Schedule unlinking of physical storage at transaction commit.
@@ -213,6 +647,7 @@ RelationDropStorage(Relation rel)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->rlocator = rel->rd_locator;
pending->procNumber = rel->rd_backend;
+ pending->createXid = InvalidTransactionId;
pending->atCommit = true; /* delete if commit */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
@@ -262,6 +697,15 @@ RelationPreserveStorage(RelFileLocator rlocator, bool atCommit)
if (RelFileLocatorEquals(rlocator, pending->rlocator)
&& pending->atCommit == atCommit)
{
+ if (!atCommit && TransactionIdIsValid(pending->createXid))
+ {
+ log_smgrpreserve(&pending->rlocator, pending->createXid);
+ append_relation_create_manifest(pending->createXid,
+ &pending->rlocator,
+ RELATION_CREATE_MANIFEST_PRESERVE,
+ true, ERROR);
+ }
+
/* unlink and delete list entry */
if (prev)
prev->next = next;
@@ -679,6 +1123,9 @@ smgrDoPendingDeletes(bool isCommit)
int nrels = 0,
maxrels = 0;
SMgrRelation *srels = NULL;
+ int ncreateXids = 0,
+ maxcreateXids = 0;
+ TransactionId *createXids = NULL;
prev = NULL;
for (pending = pendingDeletes; pending != NULL; pending = next)
@@ -716,7 +1163,38 @@ smgrDoPendingDeletes(bool isCommit)
}
srels[nrels++] = srel;
+
+ if (!isCommit && TransactionIdIsValid(pending->createXid))
+ {
+ int i;
+
+ for (i = 0; i < ncreateXids; i++)
+ if (TransactionIdEquals(createXids[i],
+ pending->createXid))
+ break;
+
+ if (i == ncreateXids)
+ {
+ if (maxcreateXids == 0)
+ {
+ maxcreateXids = 8;
+ createXids = palloc_array(TransactionId,
+ maxcreateXids);
+ }
+ else if (maxcreateXids <= ncreateXids)
+ {
+ maxcreateXids *= 2;
+ createXids = repalloc_array(createXids,
+ TransactionId,
+ maxcreateXids);
+ }
+
+ createXids[ncreateXids++] = pending->createXid;
+ }
+ }
}
+ else if (isCommit && TransactionIdIsValid(pending->createXid))
+ RelationCreateManifestCleanup(pending->createXid);
/* must explicitly free the list entry */
pfree(pending);
/* prev does not change */
@@ -732,6 +1210,11 @@ smgrDoPendingDeletes(bool isCommit)
pfree(srels);
}
+
+ for (int i = 0; i < ncreateXids; i++)
+ RelationCreateManifestCleanup(createXids[i]);
+ if (createXids != NULL)
+ pfree(createXids);
}
/*
@@ -986,7 +1469,26 @@ smgr_redo(XLogReaderState *record)
/* Backup blocks are not used in smgr records */
Assert(!XLogRecHasAnyBlockRefs(record));
- if (info == XLOG_SMGR_CREATE)
+ if (info == XLOG_SMGR_PRECREATE)
+ {
+ xl_smgr_precreate *xlrec = (xl_smgr_precreate *) XLogRecGetData(record);
+ TransactionId xid = XLogRecGetXid(record);
+
+ if (!TransactionIdIsValid(xid))
+ elog(PANIC, "relation pre-create WAL record has no transaction ID");
+ append_relation_create_manifest(xid, &xlrec->rlocator,
+ RELATION_CREATE_MANIFEST_CREATE,
+ true, PANIC);
+ }
+ else if (info == XLOG_SMGR_PRESERVE)
+ {
+ xl_smgr_preserve *xlrec = (xl_smgr_preserve *) XLogRecGetData(record);
+
+ append_relation_create_manifest(xlrec->xid, &xlrec->rlocator,
+ RELATION_CREATE_MANIFEST_PRESERVE,
+ true, PANIC);
+ }
+ else if (info == XLOG_SMGR_CREATE)
{
xl_smgr_create *xlrec = (xl_smgr_create *) XLogRecGetData(record);
SMgrRelation reln;
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 780c88c0630..cbd786a96bc 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -27,6 +27,7 @@
#include <sys/file.h>
#include "access/xlogutils.h"
+#include "catalog/storage.h"
#include "commands/tablespace.h"
#include "common/file_utils.h"
#include "miscadmin.h"
@@ -1957,6 +1958,8 @@ int
mdunlinkfiletag(const FileTag *ftag, char *path)
{
RelPathStr p;
+ int result;
+ int save_errno;
/* We only unlink tombstone files through this mechanism */
Assert(ftag->forknum == MAIN_FORKNUM && ftag->segno == 0);
@@ -1966,7 +1969,11 @@ mdunlinkfiletag(const FileTag *ftag, char *path)
strlcpy(path, p.str, MAXPGPATH);
/* Try to unlink the file. */
- return unlink(path);
+ result = unlink(path);
+ save_errno = errno;
+
+ errno = save_errno;
+ return result;
}
/*
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 86d42a98a27..a0d108226cd 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -236,6 +236,7 @@ static const char *const subdirs[] = {
"pg_commit_ts",
"pg_dynshmem",
"pg_notify",
+ "pg_relcreate",
"pg_serial",
"pg_snapshots",
"pg_subtrans",
diff --git a/src/bin/pg_rewind/parsexlog.c b/src/bin/pg_rewind/parsexlog.c
index 6e87b00f8c2..92c1cfbca56 100644
--- a/src/bin/pg_rewind/parsexlog.c
+++ b/src/bin/pg_rewind/parsexlog.c
@@ -432,6 +432,22 @@ extractPageInfo(XLogReaderState *record)
* for all the blocks in it.
*/
}
+ else if (rmid == RM_SMGR_ID && rminfo == XLOG_SMGR_PRECREATE)
+ {
+ /*
+ * We can safely ignore these. The manifest file will be copied or
+ * removed when the target data directory is synchronized with the
+ * source.
+ */
+ }
+ else if (rmid == RM_SMGR_ID && rminfo == XLOG_SMGR_PRESERVE)
+ {
+ /*
+ * We can safely ignore these. The manifest file will be copied or
+ * removed when the target data directory is synchronized with the
+ * source.
+ */
+ }
else if (rmid == RM_SMGR_ID && rminfo == XLOG_SMGR_TRUNCATE)
{
/*
diff --git a/src/include/access/twophase.h b/src/include/access/twophase.h
index 1d2ff42c9b7..152b1e0d2f6 100644
--- a/src/include/access/twophase.h
+++ b/src/include/access/twophase.h
@@ -48,6 +48,7 @@ extern GlobalTransaction MarkAsPreparing(FullTransactionId fxid, const char *gid
extern void StartPrepare(GlobalTransaction gxact);
extern void EndPrepare(GlobalTransaction gxact);
extern bool StandbyTransactionIdIsPrepared(TransactionId xid);
+extern bool TwoPhaseTransactionIdIsPrepared(TransactionId xid);
extern TransactionId PrescanPreparedTransactions(TransactionId **xids_p,
int *nxids_p);
diff --git a/src/include/access/xact.h b/src/include/access/xact.h
index a8cbdf247c8..58b62a3565f 100644
--- a/src/include/access/xact.h
+++ b/src/include/access/xact.h
@@ -121,6 +121,9 @@ extern PGDLLIMPORT int MyXactFlags;
*/
#define XACT_FLAGS_PIPELINING (1U << 3)
+/* XACT_FLAGS_HAS_RELATION_CREATE - relation creation manifests were written. */
+#define XACT_FLAGS_HAS_RELATION_CREATE (1U << 4)
+
/*
* start- and end-of-transaction callbacks for dynamically loaded modules
*/
@@ -195,6 +198,7 @@ typedef struct SavedTransactionCharacteristics
#define XACT_XINFO_HAS_AE_LOCKS (1U << 6)
#define XACT_XINFO_HAS_GID (1U << 7)
#define XACT_XINFO_HAS_DROPPED_STATS (1U << 8)
+#define XACT_XINFO_HAS_RELATION_CREATE (1U << 9)
/*
* Also stored in xinfo, these indicating a variety of additional actions that
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 70f619a6d6f..1b1bb946229 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -27,6 +27,12 @@ extern SMgrRelation RelationCreateStorage(RelFileLocator rlocator,
bool register_delete);
extern void RelationDropStorage(Relation rel);
extern void RelationPreserveStorage(RelFileLocator rlocator, bool atCommit);
+extern void RelationCreateManifestCleanup(TransactionId xid);
+extern void RelationCreateManifestCleanupTree(TransactionId xid,
+ int nsubxacts,
+ TransactionId *subxacts);
+extern void RelationCreateManifestCleanupAtCheckpoint(void);
+extern void RelationCreateManifestCleanupAtEndOfRecovery(void);
extern void RelationPreTruncate(Relation rel);
extern void RelationTruncate(Relation rel, BlockNumber nblocks);
extern void RelationCopyStorage(SMgrRelation src, SMgrRelation dst,
diff --git a/src/include/catalog/storage_xlog.h b/src/include/catalog/storage_xlog.h
index c1b2f736669..665151341e5 100644
--- a/src/include/catalog/storage_xlog.h
+++ b/src/include/catalog/storage_xlog.h
@@ -29,6 +29,19 @@
/* XLOG gives us high 4 bits */
#define XLOG_SMGR_CREATE 0x10
#define XLOG_SMGR_TRUNCATE 0x20
+#define XLOG_SMGR_PRECREATE 0x30
+#define XLOG_SMGR_PRESERVE 0x40
+
+typedef struct xl_smgr_precreate
+{
+ RelFileLocator rlocator;
+} xl_smgr_precreate;
+
+typedef struct xl_smgr_preserve
+{
+ RelFileLocator rlocator;
+ TransactionId xid;
+} xl_smgr_preserve;
typedef struct xl_smgr_create
{
@@ -51,6 +64,8 @@ typedef struct xl_smgr_truncate
} xl_smgr_truncate;
extern void log_smgrcreate(const RelFileLocator *rlocator, ForkNumber forkNum);
+extern TransactionId log_smgrprecreate(const RelFileLocator *rlocator);
+extern void log_smgrpreserve(const RelFileLocator *rlocator, TransactionId xid);
extern void smgr_redo(XLogReaderState *record);
extern void smgr_desc(StringInfo buf, XLogReaderState *record);
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 72113c5ac6e..c70ea81c070 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -65,6 +65,7 @@ tests += {
't/054_unlogged_sequence_promotion.pl',
't/055_cascade_reconnect.pl',
't/056_standby_snapshot_export.pl',
+ 't/057_relation_create_markers.pl',
],
},
}
diff --git a/src/test/recovery/t/057_relation_create_markers.pl b/src/test/recovery/t/057_relation_create_markers.pl
new file mode 100644
index 00000000000..b0501f45655
--- /dev/null
+++ b/src/test/recovery/t/057_relation_create_markers.pl
@@ -0,0 +1,187 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test cleanup of permanent relation files created by transactions that are
+# still in progress when the server crashes.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('relation_create_manifests');
+$node->init(allows_streaming => 1);
+$node->append_conf('postgresql.conf', 'max_prepared_transactions = 10');
+$node->start();
+
+my $manifest_dir = $node->data_dir . '/pg_relcreate';
+
+sub manifest_count
+{
+ my ($cluster) = @_;
+ my $dir = $cluster->data_dir . '/pg_relcreate';
+
+ return scalar(grep { $_ ne '.' && $_ ne '..' } slurp_dir($dir));
+}
+
+$node->safe_psql('postgres', 'CREATE TABLE committed_relation (a int)');
+is(manifest_count($node), 0,
+ 'committed relation leaves no creation manifest');
+
+my $rollback_session = $node->background_psql('postgres');
+$rollback_session->query_safe('BEGIN');
+$rollback_session->query_safe(
+ 'CREATE TABLE rolled_back_relation_1 (a int); '
+ . 'CREATE TABLE rolled_back_relation_2 (a int)');
+is(manifest_count($node), 1,
+ 'two relations in a transaction share one manifest before rollback');
+$rollback_session->query_safe('ROLLBACK');
+is(manifest_count($node), 0,
+ 'ordinary rollback removes relation creation manifest');
+is($node->safe_psql('postgres',
+ q{SELECT to_regclass('rolled_back_relation_1') IS NULL AND
+to_regclass('rolled_back_relation_2') IS NULL}),
+ 't', 'ordinarily aborted relations are absent from the catalog');
+
+my $subxact_session = $node->background_psql('postgres');
+$subxact_session->query_safe('BEGIN');
+$subxact_session->query_safe('CREATE TABLE top_relation (a int)');
+$subxact_session->query_safe('SAVEPOINT create_relation');
+$subxact_session->query_safe('CREATE TABLE sub_relation (a int)');
+is(manifest_count($node), 2,
+ 'top-level and subtransaction relation creations use separate manifests');
+$subxact_session->query_safe('RELEASE SAVEPOINT create_relation');
+$subxact_session->query_safe('ROLLBACK');
+is(manifest_count($node), 0,
+ 'top-level rollback removes subtransaction creation manifests');
+
+my $session = $node->background_psql('postgres');
+$session->query_safe('BEGIN');
+my @relation_paths = split /\n/, $session->query_safe(
+ 'CREATE TABLE crash_aborted_relation_1 (a int); '
+ . 'CREATE TABLE crash_aborted_relation_2 (a int); '
+ . q{SELECT pg_relation_filepath('crash_aborted_relation_1') UNION ALL }
+ . q{SELECT pg_relation_filepath('crash_aborted_relation_2')});
+
+is(scalar(grep { !-f $node->data_dir . '/' . $_ } @relation_paths), 0,
+ 'uncommitted relation files exist before crash');
+is(manifest_count($node), 1,
+ 'two relations created by one transaction share one manifest');
+
+# Move the redo pointer past the creation record. Recovery therefore needs
+# the persistent manifest; replay-local tracking of the create record is not
+# sufficient.
+$node->safe_psql('postgres', 'CHECKPOINT');
+$node->stop('immediate');
+$node->start();
+
+is($node->safe_psql('postgres',
+ q{SELECT to_regclass('crash_aborted_relation_1') IS NULL AND
+to_regclass('crash_aborted_relation_2') IS NULL}),
+ 't', 'crash-aborted relations are absent from the catalog');
+is(scalar(grep { -e $node->data_dir . '/' . $_ } @relation_paths), 0,
+ 'crash-aborted relation files are removed during recovery');
+is(manifest_count($node), 0, 'processed creation manifest is removed');
+
+my $truncated_session = $node->background_psql('postgres');
+$truncated_session->query_safe('BEGIN');
+my $truncated_relation_path = $truncated_session->query_safe(
+ q{CREATE TABLE truncated_manifest_relation (a int);
+SELECT pg_relation_filepath('truncated_manifest_relation');});
+is(manifest_count($node), 1,
+ 'relation creation writes a manifest to truncate');
+$node->safe_psql('postgres', 'CHECKPOINT');
+$node->stop('immediate');
+
+my @manifest_names =
+ grep { $_ ne '.' && $_ ne '..' } slurp_dir($manifest_dir);
+is(scalar(@manifest_names), 1, 'found manifest to truncate');
+my $truncated_manifest = $manifest_dir . '/' . $manifest_names[0];
+open(my $manifest_fh, '>>', $truncated_manifest)
+ or die "could not open $truncated_manifest: $!";
+binmode($manifest_fh);
+print {$manifest_fh} "\0";
+close($manifest_fh) or die "could not close $truncated_manifest: $!";
+
+ok(!$node->start(fail_ok => 1),
+ 'startup rejects a truncated relation creation manifest');
+truncate($truncated_manifest, (-s $truncated_manifest) - 1)
+ or die "could not repair $truncated_manifest: $!";
+$node->start();
+ok(!-e $node->data_dir . '/' . $truncated_relation_path,
+ 'repaired manifest removes the crash-aborted relation file');
+is(manifest_count($node), 0, 'repaired manifest is removed');
+
+my $prepared_path = $node->safe_psql(
+ 'postgres',
+ q{BEGIN;
+CREATE TABLE prepared_relation (a int);
+SELECT pg_relation_filepath('prepared_relation');
+PREPARE TRANSACTION 'relation_create_marker';});
+ok(-f $node->data_dir . '/' . $prepared_path,
+ 'prepared relation file exists');
+is(manifest_count($node), 1,
+ 'prepared relation retains its creation manifest');
+
+$node->stop('immediate');
+$node->start();
+
+ok(-f $node->data_dir . '/' . $prepared_path,
+ 'prepared relation file survives recovery');
+is(manifest_count($node), 1,
+ 'recovery retains prepared relation manifest');
+$node->safe_psql('postgres',
+ q{COMMIT PREPARED 'relation_create_marker'});
+is($node->safe_psql('postgres',
+ q{SELECT to_regclass('prepared_relation') IS NOT NULL}),
+ 't', 'committed prepared relation is visible');
+is(manifest_count($node), 0,
+ 'commit prepared removes relation manifest');
+
+$node->safe_psql(
+ 'postgres',
+ q{BEGIN;
+CREATE TABLE aborted_prepared_relation (a int);
+PREPARE TRANSACTION 'relation_create_manifest_abort';});
+is(manifest_count($node), 1,
+ 'prepared transaction to abort retains its manifest');
+$node->safe_psql('postgres',
+ q{ROLLBACK PREPARED 'relation_create_manifest_abort'});
+is(manifest_count($node), 0,
+ 'rollback prepared removes relation manifest');
+
+$node->backup('manifest_backup');
+my $standby = PostgreSQL::Test::Cluster->new('relation_create_standby');
+$standby->init_from_backup($node, 'manifest_backup', has_streaming => 1);
+$standby->start();
+
+my $commit_session = $node->background_psql('postgres');
+$commit_session->query_safe('BEGIN');
+$commit_session->query_safe(
+ 'CREATE TABLE standby_committed_relation_1 (a int); '
+ . 'CREATE TABLE standby_committed_relation_2 (a int)');
+$node->safe_psql('postgres', 'SELECT pg_switch_wal()');
+$node->wait_for_catchup($standby);
+is(manifest_count($standby), 1,
+ 'standby uses one manifest for two relations from one transaction');
+$commit_session->query_safe('COMMIT');
+$node->wait_for_catchup($standby);
+is(manifest_count($standby), 0,
+ 'commit replay removes standby relation creation manifest');
+
+my $abort_session = $node->background_psql('postgres');
+$abort_session->query_safe('BEGIN');
+$abort_session->query_safe('CREATE TABLE standby_aborted_relation (a int)');
+$node->safe_psql('postgres', 'SELECT pg_switch_wal()');
+$node->wait_for_catchup($standby);
+is(manifest_count($standby), 1,
+ 'standby retains manifest for an in-progress transaction');
+$abort_session->query_safe('ROLLBACK');
+$node->safe_psql('postgres', 'SELECT pg_switch_wal()');
+$node->wait_for_catchup($standby);
+is(manifest_count($standby), 0,
+ 'abort replay removes standby relation creation manifest');
+
+$standby->stop();
+$node->stop();
+done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-23 12:38 Andrey Borodin <x4mmm@yandex-team.ru>
parent: Ashutosh Sharma <ashu.coek88@gmail.com>
0 siblings, 3 replies; 12+ messages in thread
From: Andrey Borodin @ 2026-09-23 12:38 UTC (permalink / raw)
To: Ashutosh Sharma <ashu.coek88@gmail.com>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers; Andres Freund <andres@anarazel.de>
Hi Ashutosh,
On 23 Sep 2026, Ashutosh Sharma wrote:
> Please take a look and let me know.
At the design level, one manifest per XID still means a pg_fsync() for
each appended record, including during redo. Have you considered WAL
plus delayed manifest synchronization, along the lines Andres
suggested [0]? It would be useful to compare small-DDL and replay
costs before settling on synchronous per-record writes.
From reading v2, I am concerned about mapped catalog rewrites.
write_relmap_file() flushes XLOG_RELMAP_UPDATE before calling
RelationPreserveStorage(), where the patch now records PRESERVE.
A crash between those steps leaves the new mapping durable, but the
creating transaction uncommitted and its manifest without PRESERVE.
relmap_redo() does not preserve the storage either. Wouldn't the new
end-of-recovery cleanup then remove files needed by the mapped catalog?
Could preservation be part of the relmap update's recovery semantics?
A crash test in that window during VACUUM FULL of a mapped catalog
seems particularly important. I haven't run that reproducer yet.
The truncated-manifest test expects startup to fail. Can a crash during
a normal append leave that state without replay repairing it? If so,
could we retain the uncertain files rather than refuse startup?
Also, Greg recently mentioned renewed UNDO/FILEOPS work [1]. It may
be worth coordinating the scope with him. Preventing new orphans and
handling existing ones, as needed for online checksums, are separate
parts of the problem.
Thank you!
Best regards, Andrey Borodin.
[0] https://postgr.es/m/20170814185632.zodm5qykgss7ud32@alap3.anarazel.de
[1] https://postgr.es/m/5d89549c-117e-45ae-b934-a2bb71c82a79@app.fastmail.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-23 18:25 Greg Burd <greg@burd.me>
parent: Andrey Borodin <x4mmm@yandex-team.ru>
2 siblings, 1 reply; 12+ messages in thread
From: Greg Burd @ 2026-09-23 18:25 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: Ashutosh Sharma <ashu.coek88@gmail.com>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers; Andres Freund <andres@anarazel.de>
On Wednesday, September 23rd, 2026 at 8:39 AM, Andrey Borodin <x4mmm@yandex-team.ru> wrote:
> Hi Ashutosh,
>
> On 23 Sep 2026, Ashutosh Sharma wrote:
> > Please take a look and let me know.
Hey Ashutosh!
Thanks for the excellent email kicking off this thread, I couldn't agree
more about the issue. I started in a different place, asking myself if
I could resurrect UNDO from ZHEAP without modifying HEAP at all (because
I tried and it didn't help, other different table AMs might find benefit
but HEAP is rather solid as it is) and if I could how would I demonstrate
and justify it without adding a new table AM or modifying HEAP?
I used to work on Berkeley DB and one of the features was its ability to
WAL log UNDO/REDO records for "filesystem operations" and then during
recovery tidy up and make things consistent. That struck me as a solid
first application of UNDO in Postgres, so I created that and called it
FILEOPS (because I'm not creative at all).
> At the design level, one manifest per XID still means a pg_fsync() for
> each appended record, including during redo. Have you considered WAL
> plus delayed manifest synchronization, along the lines Andres
> suggested [0]? It would be useful to compare small-DDL and replay
> costs before settling on synchronous per-record writes.
>
> From reading v2, I am concerned about mapped catalog rewrites.
> write_relmap_file() flushes XLOG_RELMAP_UPDATE before calling
> RelationPreserveStorage(), where the patch now records PRESERVE.
> A crash between those steps leaves the new mapping durable, but the
> creating transaction uncommitted and its manifest without PRESERVE.
> relmap_redo() does not preserve the storage either. Wouldn't the new
> end-of-recovery cleanup then remove files needed by the mapped catalog?
>
> Could preservation be part of the relmap update's recovery semantics?
> A crash test in that window during VACUUM FULL of a mapped catalog
> seems particularly important. I haven't run that reproducer yet.
>
> The truncated-manifest test expects startup to fail. Can a crash during
> a normal append leave that state without replay repairing it? If so,
> could we retain the uncertain files rather than refuse startup?
>
> Also, Greg recently mentioned renewed UNDO/FILEOPS work [1]. It may
> be worth coordinating the scope with him. Preventing new orphans and
> handling existing ones, as needed for online checksums, are separate
> parts of the problem.
I'm spending a lot of time this week cleaning up the UNDO and FILEOPS
patches in hopes of posting them soon as either an RFC or a proposed
patch set. I do have new table AMs that use it, but I don't think
they are ready for prime time yet. There are other use cases for UNDO
also like BLOB/CLOBs etc. that I think might be interesting and if I
can get the integration with nbtree and hash correct there are also
benefits for indexes.
That said, it's a large change and one that has philosophical and
technical challenges before the community could even consider merging
it in. I have hope, but it'll be a long road.
Your approach has less overhead/history to deal with. I'll need to
dig into it more to appreciate the direction you've taken but I do
agree that it needs to happen somehow and so I don't see this as a
competing idea at all.
best.
-greg
> Thank you!
>
>
> Best regards, Andrey Borodin.
>
> [0] https://postgr.es/m/20170814185632.zodm5qykgss7ud32@alap3.anarazel.de
> [1] https://postgr.es/m/5d89549c-117e-45ae-b934-a2bb71c82a79@app.fastmail.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-23 22:10 Zsolt Parragi <zsolt.parragi@percona.com>
parent: Andrey Borodin <x4mmm@yandex-team.ru>
2 siblings, 1 reply; 12+ messages in thread
From: Zsolt Parragi @ 2026-09-23 22:10 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: pgsql-hackers@lists.postgresql.org, Ashutosh Sharma <ashu.coek88@gmail.com>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Hello!
This issue is also related to a recent discussion about online
checksums[1] and I tried to look into possible solutions into it when
investigating that, and I agree that it should be improved.
But I think the proposed patch has some issues.
On Wed, 23 Sep 2026, Andrey Borodin <amborodin@acm.org> wrote:
> From reading v2, I am concerned about mapped catalog rewrites.
> write_relmap_file() flushes XLOG_RELMAP_UPDATE before calling
> RelationPreserveStorage(), where the patch now records PRESERVE.
> A crash between those steps leaves the new mapping durable, but the
> creating transaction uncommitted and its manifest without PRESERVE.
It doesn't need a random crash, PITR to a VACUUM FULL
pg_class/pg_database/... with recovery_target_action = 'promote' can
crash the server / completely brick the datadir.
For example if waldump shows:
Storage 0/030380B0 PRECREATE base/5/16387
RelMap 0/03040E48 UPDATE database ...
Storage 0/03041080 PRESERVE base/5/16384 xid 664
Storage 0/030410B0 PRESERVE base/5/16387 xid 664
Transaction 0/03041140 COMMIT
Then PITR to that RelMap entry reproduces the issue.
Another recovery issue is that end of recovery reconciliation runs
after the cluster already left recovery, and it will unlink the
storage of live transactions.
For example retrying BEGIN; CREATE TABLE ... ; loops accross a pg_ctl
promote pulls the storage out from the new tables, the transactions
can still COMMIT, and then access to that table fails because there's
no storage for it.
@@ -262,6 +697,15 @@ RelationPreserveStorage(RelFileLocator rlocator,
bool atCommit)
if (RelFileLocatorEquals(rlocator, pending->rlocator)
&& pending->atCommit == atCommit)
{
+ if (!atCommit && TransactionIdIsValid(pending->createXid))
+ {
This performs IO inside a critical section and can panic the server if
that IO fails. And if that panic happens with a catalog table, the
database can't start up again, because recovery unlinks the new file.
> The patch also handles subtransactions, prepared transactions, standby
> replay, truncated manifests, and failures that leave retired manifests
> behind.
It seems to me that the standby keeps crash aborted orphan files, only
the primary deletes them properly.
> It would be useful to compare small-DDL and replay
> costs before settling on synchronous per-record writes.
Other than costs, currently postgres honors synchronous_commit = off
for storage creating DDL. With the patch, it no longer does so. That
at least needs proper documentation, but I am not so sure that this is
an actual requirement for preventing orphan files.
[1] : https://www.postgresql.org/message-id/CA%2BTgmoaOCdjAjr240e_%2BxoqQCRmLC9MFn3kZOu-7j8w8HBrd8g%40mail...
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-24 08:00 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Andrey Borodin <x4mmm@yandex-team.ru>
2 siblings, 0 replies; 12+ messages in thread
From: Ashutosh Sharma @ 2026-09-24 08:00 UTC (permalink / raw)
To: Andrey Borodin <x4mmm@yandex-team.ru>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers; Andres Freund <andres@anarazel.de>
Hi,
Thank you for taking a quick look at the proposed changes and sharing
your feedback.
On Wed, Sep 23, 2026 at 6:09 PM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
>
> Hi Ashutosh,
>
> On 23 Sep 2026, Ashutosh Sharma wrote:
> > Please take a look and let me know.
>
> At the design level, one manifest per XID still means a pg_fsync() for
> each appended record, including during redo. Have you considered WAL
> plus delayed manifest synchronization, along the lines Andres
> suggested [0]? It would be useful to compare small-DDL and replay
> costs before settling on synchronous per-record writes.
>
> From reading v2, I am concerned about mapped catalog rewrites.
> write_relmap_file() flushes XLOG_RELMAP_UPDATE before calling
> RelationPreserveStorage(), where the patch now records PRESERVE.
> A crash between those steps leaves the new mapping durable, but the
> creating transaction uncommitted and its manifest without PRESERVE.
> relmap_redo() does not preserve the storage either. Wouldn't the new
> end-of-recovery cleanup then remove files needed by the mapped catalog?
>
> Could preservation be part of the relmap update's recovery semantics?
> A crash test in that window during VACUUM FULL of a mapped catalog
> seems particularly important. I haven't run that reproducer yet.
>
> The truncated-manifest test expects startup to fail. Can a crash during
> a normal append leave that state without replay repairing it? If so,
> could we retain the uncertain files rather than refuse startup?
>
> Also, Greg recently mentioned renewed UNDO/FILEOPS work [1]. It may
> be worth coordinating the scope with him. Preventing new orphans and
> handling existing ones, as needed for online checksums, are separate
> parts of the problem.
>
> Thank you!
>
>
> Best regards, Andrey Borodin.
>
> [0] https://postgr.es/m/20170814185632.zodm5qykgss7ud32@alap3.anarazel.de
> [1] https://postgr.es/m/5d89549c-117e-45ae-b934-a2bb71c82a79@app.fastmail.com
>
I have not yet reviewed the earlier discussions in these threads in
enough detail. It appears that the approach I am proposing overlaps
with Chris Travers earlier work, but I do not yet understand why that
work did not proceed, whether because of unresolved technical issues
or other considerations. I will review those discussions before
deciding how to revise this proposal.
The concern raised about mapped relation files looks valid and
requires additional handling. However, before working out that fix, I
would first like to understand the earlier proposals and reconsider
the overall strategy in that context.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-24 08:07 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Greg Burd <greg@burd.me>
0 siblings, 0 replies; 12+ messages in thread
From: Ashutosh Sharma @ 2026-09-24 08:07 UTC (permalink / raw)
To: Greg Burd <greg@burd.me>; +Cc: Andrey Borodin <x4mmm@yandex-team.ru>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers; Andres Freund <andres@anarazel.de>; Chris.travers@adjust.com
Hi,
On Wed, Sep 23, 2026 at 11:55 PM Greg Burd <greg@burd.me> wrote:
> Hey Ashutosh!
>
> Thanks for the excellent email kicking off this thread, I couldn't agree
> more about the issue.
Thanks to you too for working on this project. I am glad to know that
I am not alone on this path of finding a solution to this problem.
I started in a different place, asking myself if
> I could resurrect UNDO from ZHEAP without modifying HEAP at all (because
> I tried and it didn't help, other different table AMs might find benefit
> but HEAP is rather solid as it is) and if I could how would I demonstrate
> and justify it without adding a new table AM or modifying HEAP?
>
> I used to work on Berkeley DB and one of the features was its ability to
> WAL log UNDO/REDO records for "filesystem operations" and then during
> recovery tidy up and make things consistent. That struck me as a solid
> first application of UNDO in Postgres, so I created that and called it
> FILEOPS (because I'm not creative at all).
>
Thanks for sharing all of this. I will surely go through it once you
make it public.
>
> I'm spending a lot of time this week cleaning up the UNDO and FILEOPS
> patches in hopes of posting them soon as either an RFC or a proposed
> patch set. I do have new table AMs that use it, but I don't think
> they are ready for prime time yet. There are other use cases for UNDO
> also like BLOB/CLOBs etc. that I think might be interesting and if I
> can get the integration with nbtree and hash correct there are also
> benefits for indexes.
>
Sure, please share it, and I will be happy to contribute in whatever
way I can. I see that you shared this information earlier here [0],
but I somehow missed it, perhaps because I was not part of that
discussion or did not realize that it was related to the work being
discussed here.
> That said, it's a large change and one that has philosophical and
> technical challenges before the community could even consider merging
> it in. I have hope, but it'll be a long road.
>
Yes, that seems to be pretty obvious considering the complexity involved here.
> Your approach has less overhead/history to deal with. I'll need to
> dig into it more to appreciate the direction you've taken but I do
> agree that it needs to happen somehow and so I don't see this as a
> competing idea at all.
>
Well, it seems like this approach has already been discussed/proposed
earlier here [1] by Chris but somehow it didn't move forward. I still
need to understand the details and any blockers that prevent it from
progressing.
[0] - https://www.postgresql.org/message-id/5d89549c-117e-45ae-b934-a2bb71c82a79%40app.fastmail.com
[1] - https://www.postgresql.org/message-id/CAN-RpxDBA7HbTsJPq4t4VznmRFJkssP2SNEMuG%3DoNJ%2B%3DsxLQew%40ma...
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Orphaned Files in PostgreSQL
@ 2026-09-24 08:12 Ashutosh Sharma <ashu.coek88@gmail.com>
parent: Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 0 replies; 12+ messages in thread
From: Ashutosh Sharma @ 2026-09-24 08:12 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: Andrey Borodin <x4mmm@yandex-team.ru>; pgsql-hackers@lists.postgresql.org, Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Gregory Burd <greg@burd.me>
Hi,
Thanks for taking a quick look at the proposed changes and sharing
your review comments.
On Thu, Sep 24, 2026 at 3:40 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
>
> Hello!
>
> This issue is also related to a recent discussion about online
> checksums[1] and I tried to look into possible solutions into it when
> investigating that, and I agree that it should be improved.
>
Yes, there is some connection. I haven't had a chance to go through
the previous discussions yet. Let me first go through all the past
discussions related to this and then reconsider the approach.
I am also glad to learn from this discussion that Greg, as mentioned
in his earlier response in this thread, is working on a solution to
this problem as well. We can wait for his solution to become available
and see how things progress from there.
--
With Regards,
Ashutosh Sharma.
>
> [1] : https://www.postgresql.org/message-id/CA%2BTgmoaOCdjAjr240e_%2BxoqQCRmLC9MFn3kZOu-7j8w8HBrd8g%40mail...
^ permalink raw reply [nested|flat] 12+ messages in thread
end of thread, other threads:[~2026-09-24 08:12 UTC | newest]
Thread overview: 12+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-02-18 10:46 Orphaned Files in PostgreSQL Ashutosh Sharma <ashu.coek88@gmail.com>
2025-02-18 11:17 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-08-20 09:10 ` Ashutosh Sharma <ashu.coek88@gmail.com>
2026-08-21 06:55 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-08-21 09:51 ` Ashutosh Sharma <ashu.coek88@gmail.com>
2026-09-23 11:18 ` Ashutosh Sharma <ashu.coek88@gmail.com>
2026-09-23 12:38 ` Andrey Borodin <x4mmm@yandex-team.ru>
2026-09-23 18:25 ` Greg Burd <greg@burd.me>
2026-09-24 08:07 ` Ashutosh Sharma <ashu.coek88@gmail.com>
2026-09-23 22:10 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-24 08:12 ` Ashutosh Sharma <ashu.coek88@gmail.com>
2026-09-24 08:00 ` Ashutosh Sharma <ashu.coek88@gmail.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox