From ae8e1116951f86cac5fd88aea11231848626c8b8 Mon Sep 17 00:00:00 2001 From: Ashutosh Sharma 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 Subdirectory containing replication slot data + + pg_relcreate + Subdirectory containing durable manifests for transactional relation + creation + + pg_serial Subdirectory containing information about committed serializable transactions 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 #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