Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1rpJDx-0065y6-OR for pgsql-hackers@arkaria.postgresql.org; Wed, 27 Mar 2024 02:41:10 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1rpJDv-008qZ9-JZ for pgsql-hackers@arkaria.postgresql.org; Wed, 27 Mar 2024 02:41:07 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1rpJDu-008qY0-Lh for pgsql-hackers@lists.postgresql.org; Wed, 27 Mar 2024 02:41:07 +0000 Received: from mail-io1-xd35.google.com ([2607:f8b0:4864:20::d35]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1rpJDh-006czE-CE for pgsql-hackers@postgresql.org; Wed, 27 Mar 2024 02:41:05 +0000 Received: by mail-io1-xd35.google.com with SMTP id ca18e2360f4ac-7c897ac777aso152184139f.2 for ; Tue, 26 Mar 2024 19:40:54 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1711507254; x=1712112054; darn=postgresql.org; h=in-reply-to:content-disposition:mime-version:references:message-id :subject:cc:to:from:date:from:to:cc:subject:date:message-id:reply-to; bh=To9Qvo5WsmPqvbZLiYS2ZBEIAOSN5BUfqcKEuTvhK1s=; b=gpPDcVsdoXuivx6P4SOk8dBSf0HlQOp/kchDjMKAP7IEPscPQdniE7deK77qU9qFp5 CWfPr6eVJ1wCIPbmtIkXqu/8scbANjNHBo0vGfqFrDSc8CLTAKvFbH0PGv+BCR/KzOAG fz0fdzqtrmd6P5fMyAhZUwykhZdI5z5UGWVOwG9GBalTz56mlU8ThP8E5CnZJE2vWsQI 0MTSkUJLG8aFkPtbVAG/2QQp6Om4Gl74IB9rV/BKY7MHwPgEKWD1VxSXSDZqJAj8nr8X B7iCR37eFeQp8B2jAfOqZ4lsOde4t4wdDE2H4OtxZBOfVQ/QP7ZpUfDenGRdiJbrtEwO f2Ww== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1711507254; x=1712112054; h=in-reply-to:content-disposition:mime-version:references:message-id :subject:cc:to:from:date:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=To9Qvo5WsmPqvbZLiYS2ZBEIAOSN5BUfqcKEuTvhK1s=; b=Cdz5S8qkfTTG0r5sk/a2pFhZCXe9HE+nHpk1KvfHTa3Px8ijNi/0K9+vUPLqE+BpLN CFzxTNLemhe1j0affi+XDytT+NAZzVB/9Tnn+KXbC0CHVnWTgfXbUffRP9ZnGY3AxzLY /B6Bb5uFKwesc8nhhhhxg7r97d9LVcqK+Ub3VhAOgDT43OFzYNvrvKI5gqRAAcMZSQ2z zmPr2htFa67xWabpQPt9Qf/rUmDKaw8lTGmkNdtLB5KXpb8gX1u6d+vrpkIaWdIRbWT+ zBMOzpJLzDqbNN4J21imBQAEcYNfgg2S8hJUG9d172znl8EFQu7rsBvaMsdarYEwLPsa /vmQ== X-Forwarded-Encrypted: i=1; AJvYcCUsH7Leew9+47G7R50vPUbBv/oT/TBT1yg5hKEq09EFiF9lsWrkvGKM/tVTfBCVRqNC+M9ORvonFnTdy+J6EiW33kPu9QWaTNO61/iW X-Gm-Message-State: AOJu0YxSUmqC8Qr5qSuBV0tlBRQhOdn6VqVpLv6ZgqpZlum99HxkgqO7 Lz4yhxiTYfVKo9sZacLtG6Fx3w0/buzhFCg3AVbgkLeTXsvnQxw7 X-Google-Smtp-Source: AGHT+IGWSBvIVBdPVxv5ulP5+K0YBjY/2/hiDpIeuWYVvV1GRB5zeclWHUDuiGWUzO0ncVnip7jVqg== X-Received: by 2002:a6b:6d0d:0:b0:7cc:5d3:3c9 with SMTP id a13-20020a6b6d0d000000b007cc05d303c9mr13456476iod.8.1711507253628; Tue, 26 Mar 2024 19:40:53 -0700 (PDT) Received: from nathanxps13 (162-195-168-172.lightspeed.stlsmo.sbcglobal.net. [162.195.168.172]) by smtp.gmail.com with ESMTPSA id k5-20020a5d8745000000b007cc0b64c1fdsm3197687iol.16.2024.03.26.19.40.52 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 26 Mar 2024 19:40:52 -0700 (PDT) Date: Tue, 26 Mar 2024 21:40:50 -0500 From: Nathan Bossart To: Michael Paquier Cc: Andres Freund , Robert Haas , pgsql-hackers@postgresql.org Subject: Re: recovery modules Message-ID: <20240327024050.GA3991433@nathanxps13> References: <20230216201512.GA2074541@nathanxps13> <20230216211754.7z2v5i7gh3xahikl@awork3.anarazel.de> <20230216215810.GA2356244@nathanxps13> <20230217194132.4tim6duprvvtiu67@awork3.anarazel.de> <20230417161914.GA1123596@nathanxps13> <20231023200801.GA679157@nathanxps13> <20240111165633.GA3453473@nathanxps13> <20240306164646.GA3666655@nathanxps13> <20240315141607.GA3391357@nathanxps13> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="3V7upXqbjpZ4EhLz" Content-Disposition: inline In-Reply-To: <20240315141607.GA3391357@nathanxps13> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --3V7upXqbjpZ4EhLz Content-Type: text/plain; charset=us-ascii Content-Disposition: inline another rebase for cfbot -- Nathan Bossart Amazon Web Services: https://aws.amazon.com --3V7upXqbjpZ4EhLz Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v20-0001-introduce-routine-for-checking-mutually-exclusiv.patch" From 0ccdbb0a010830380e6ff4b7a052198d50f0680f Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 15 Feb 2023 14:28:53 -0800 Subject: [PATCH v20 1/5] introduce routine for checking mutually exclusive string GUCs --- src/backend/postmaster/pgarch.c | 8 +++----- src/backend/utils/misc/guc.c | 22 ++++++++++++++++++++++ src/include/utils/guc.h | 3 +++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index c266904b57..55e9a1796b 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -821,11 +821,9 @@ LoadArchiveLibrary(void) { ArchiveModuleInit archive_init; - if (XLogArchiveLibrary[0] != '\0' && XLogArchiveCommand[0] != '\0') - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("both archive_command and archive_library set"), - errdetail("Only one of archive_command, archive_library may be set."))); + (void) CheckMutuallyExclusiveStringGUCs(XLogArchiveLibrary, "archive_library", + XLogArchiveCommand, "archive_command", + ERROR); /* * If shell archiving is enabled, use our special initialization function. diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 391866145e..ac507008ce 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -2659,6 +2659,28 @@ ReportGUCOption(struct config_generic *record) pfree(val); } +/* + * If both parameters are set, emits a log message at 'elevel' and returns + * false. Otherwise, returns true. + */ +bool +CheckMutuallyExclusiveStringGUCs(const char *p1val, const char *p1name, + const char *p2val, const char *p2name, + int elevel) +{ + if (p1val[0] != '\0' && p2val[0] != '\0') + { + ereport(elevel, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot set both %s and %s", p1name, p2name), + errdetail("Only one of %s or %s may be set.", + p1name, p2name))); + return false; + } + + return true; +} + /* * Convert a value from one of the human-friendly units ("kB", "min" etc.) * to the given base unit. 'value' and 'unit' are the input value and unit diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index 3712aba09b..3088ada610 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -376,6 +376,9 @@ extern void RestrictSearchPath(void); extern void AtEOXact_GUC(bool isCommit, int nestLevel); extern void BeginReportingGUCOptions(void); extern void ReportChangedGUCOptions(void); +extern bool CheckMutuallyExclusiveStringGUCs(const char *p1val, const char *p1name, + const char *p2val, const char *p2name, + int elevel); extern void ParseLongOption(const char *string, char **name, char **value); extern const char *get_config_unit_name(int flags); extern bool parse_int(const char *value, int *result, int flags, -- 2.25.1 --3V7upXqbjpZ4EhLz Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v20-0002-refactor-code-for-restoring-via-shell.patch" From 9d36df03e0eece1a08b7e778e6d4c82b2228582a Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 15 Feb 2023 10:36:00 -0800 Subject: [PATCH v20 2/5] refactor code for restoring via shell --- src/backend/Makefile | 2 +- src/backend/access/transam/timeline.c | 12 +- src/backend/access/transam/xlog.c | 50 ++++- src/backend/access/transam/xlogarchive.c | 167 ++++----------- src/backend/access/transam/xlogrecovery.c | 3 +- src/backend/meson.build | 1 + src/backend/postmaster/startup.c | 16 +- src/backend/restore/Makefile | 18 ++ src/backend/restore/meson.build | 5 + src/backend/restore/shell_restore.c | 245 ++++++++++++++++++++++ src/include/Makefile | 2 +- src/include/access/xlogarchive.h | 9 +- src/include/meson.build | 1 + src/include/postmaster/startup.h | 1 + src/include/restore/shell_restore.h | 26 +++ src/tools/pgindent/typedefs.list | 1 + 16 files changed, 407 insertions(+), 152 deletions(-) create mode 100644 src/backend/restore/Makefile create mode 100644 src/backend/restore/meson.build create mode 100644 src/backend/restore/shell_restore.c create mode 100644 src/include/restore/shell_restore.h diff --git a/src/backend/Makefile b/src/backend/Makefile index 6700aec039..590b5002c0 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -19,7 +19,7 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = access archive backup bootstrap catalog parser commands executor \ foreign lib libpq \ main nodes optimizer partitioning port postmaster \ - regex replication rewrite \ + regex replication restore rewrite \ statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ jit diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index 146751ae37..3269547de7 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -59,7 +59,8 @@ restoreTimeLineHistoryFiles(TimeLineID begin, TimeLineID end) continue; TLHistoryFileName(histfname, tli); - if (RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false)) + if (RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY)) KeepFileRestoredFromArchive(path, histfname); } } @@ -97,7 +98,8 @@ readTimeLineHistory(TimeLineID targetTLI) { TLHistoryFileName(histfname, targetTLI); fromArchive = - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY); } else TLHistoryFilePath(path, targetTLI); @@ -232,7 +234,8 @@ existsTimeLineHistory(TimeLineID probeTLI) if (ArchiveRecoveryRequested) { TLHistoryFileName(histfname, probeTLI); - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS); } else TLHistoryFilePath(path, probeTLI); @@ -334,7 +337,8 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, if (ArchiveRecoveryRequested) { TLHistoryFileName(histfname, parentTLI); - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY); } else TLHistoryFilePath(path, parentTLI); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 20a5f86209..e5d12f1d15 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -83,6 +83,7 @@ #include "replication/snapbuild.h" #include "replication/walreceiver.h" #include "replication/walsender.h" +#include "restore/shell_restore.h" #include "storage/bufmgr.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -698,6 +699,7 @@ static char *GetXLogBuffer(XLogRecPtr ptr, TimeLineID tli); static XLogRecPtr XLogBytePosToRecPtr(uint64 bytepos); static XLogRecPtr XLogBytePosToEndRecPtr(uint64 bytepos); static uint64 XLogRecPtrToBytePos(XLogRecPtr ptr); +static void GetOldestRestartPointFileName(char *fname); static void WALInsertLockAcquire(void); static void WALInsertLockAcquireExclusive(void); @@ -5177,12 +5179,18 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog, { /* * Execute the recovery_end_command, if any. + * + * The command is provided the archive file cutoff point for use during + * log shipping replication. All files earlier than this point can be + * deleted from the archive, though there is no requirement to do so. */ - if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0) - ExecuteRecoveryCommand(recoveryEndCommand, - "recovery_end_command", - true, - WAIT_EVENT_RECOVERY_END_COMMAND); + if (shell_recovery_end_configured()) + { + char lastRestartPointFname[MAXFNAMELEN]; + + GetOldestRestartPointFileName(lastRestartPointFname); + shell_recovery_end(lastRestartPointFname); + } /* * We switched to a new timeline. Clean up segments on the old timeline. @@ -7666,12 +7674,18 @@ CreateRestartPoint(int flags) /* * Finally, execute archive_cleanup_command, if any. + * + * The command is provided the archive file cutoff point for use during + * log shipping replication. All files earlier than this point can be + * deleted from the archive, though there is no requirement to do so. */ - if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0) - ExecuteRecoveryCommand(archiveCleanupCommand, - "archive_cleanup_command", - false, - WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND); + if (shell_archive_cleanup_configured()) + { + char lastRestartPointFname[MAXFNAMELEN]; + + GetOldestRestartPointFileName(lastRestartPointFname); + shell_archive_cleanup(lastRestartPointFname); + } return true; } @@ -9301,6 +9315,22 @@ GetOldestRestartPoint(XLogRecPtr *oldrecptr, TimeLineID *oldtli) LWLockRelease(ControlFileLock); } +/* + * Returns the WAL file name for the last checkpoint or restartpoint. This is + * the oldest WAL file that we still need if we have to restart recovery. + */ +static void +GetOldestRestartPointFileName(char *fname) +{ + XLogRecPtr restartRedoPtr; + TimeLineID restartTli; + XLogSegNo restartSegNo; + + GetOldestRestartPoint(&restartRedoPtr, &restartTli); + XLByteToSeg(restartRedoPtr, restartSegNo, wal_segment_size); + XLogFileName(fname, restartTli, restartSegNo, wal_segment_size); +} + /* Thin wrapper around ShutdownWalRcv(). */ void XLogShutdownWalRcv(void) diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index caa1f03d93..5756f52124 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -23,12 +23,12 @@ #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "common/archive.h" -#include "common/percentrepl.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/pgarch.h" #include "postmaster/startup.h" #include "replication/walsender.h" +#include "restore/shell_restore.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -53,12 +53,11 @@ bool RestoreArchivedFile(char *path, const char *xlogfname, const char *recovername, off_t expectedSize, - bool cleanupEnabled) + bool cleanupEnabled, ArchiveType archive_type) { char xlogpath[MAXPGPATH]; - char *xlogRestoreCmd; char lastRestartPointFname[MAXPGPATH]; - int rc; + bool ret = false; struct stat stat_buf; XLogSegNo restartSegNo; XLogRecPtr restartRedoPtr; @@ -72,8 +71,21 @@ RestoreArchivedFile(char *path, const char *xlogfname, goto not_available; /* In standby mode, restore_command might not be supplied */ - if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0) - goto not_available; + switch (archive_type) + { + case ARCHIVE_TYPE_WAL_SEGMENT: + if (!shell_restore_file_configured()) + goto not_available; + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY: + if (!shell_restore_file_configured()) + goto not_available; + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: + if (!shell_restore_file_configured()) + goto not_available; + break; + } /* * When doing archive recovery, we always prefer an archived log file even @@ -149,39 +161,33 @@ RestoreArchivedFile(char *path, const char *xlogfname, else XLogFileName(lastRestartPointFname, 0, 0, wal_segment_size); - /* Build the restore command to execute */ - xlogRestoreCmd = BuildRestoreCommand(recoveryRestoreCommand, - xlogpath, xlogfname, - lastRestartPointFname); - - ereport(DEBUG3, - (errmsg_internal("executing restore command \"%s\"", - xlogRestoreCmd))); - - fflush(NULL); - pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND); - /* - * PreRestoreCommand() informs the SIGTERM handler for the startup process - * that it should proc_exit() right away. This is done for the duration - * of the system() call because there isn't a good way to break out while - * it is executing. Since we might call proc_exit() in a signal handler, - * it is best to put any additional logic before or after the - * PreRestoreCommand()/PostRestoreCommand() section. + * To ensure we are responsive to server shutdown, check for shutdown + * requests before and after restoring a file. If there is one, we exit + * right away. */ - PreRestoreCommand(); + HandleStartupProcShutdownRequests(); /* * Copy xlog from archival storage to XLOGDIR */ - rc = system(xlogRestoreCmd); - - PostRestoreCommand(); + switch (archive_type) + { + case ARCHIVE_TYPE_WAL_SEGMENT: + ret = shell_restore_wal_segment(xlogfname, xlogpath, + lastRestartPointFname); + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY: + ret = shell_restore_timeline_history(xlogfname, xlogpath); + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: + ret = shell_restore_timeline_history(xlogfname, xlogpath); + break; + } - pgstat_report_wait_end(); - pfree(xlogRestoreCmd); + HandleStartupProcShutdownRequests(); - if (rc == 0) + if (ret) { /* * command apparently succeeded, but let's make sure the file is @@ -237,37 +243,6 @@ RestoreArchivedFile(char *path, const char *xlogfname, } } - /* - * Remember, we rollforward UNTIL the restore fails so failure here is - * just part of the process... that makes it difficult to determine - * whether the restore failed because there isn't an archive to restore, - * or because the administrator has specified the restore program - * incorrectly. We have to assume the former. - * - * However, if the failure was due to any sort of signal, it's best to - * punt and abort recovery. (If we "return false" here, upper levels will - * assume that recovery is complete and start up the database!) It's - * essential to abort on child SIGINT and SIGQUIT, because per spec - * system() ignores SIGINT and SIGQUIT while waiting; if we see one of - * those it's a good bet we should have gotten it too. - * - * On SIGTERM, assume we have received a fast shutdown request, and exit - * cleanly. It's pure chance whether we receive the SIGTERM first, or the - * child process. If we receive it first, the signal handler will call - * proc_exit, otherwise we do it here. If we or the child process received - * SIGTERM for any other reason than a fast shutdown request, postmaster - * will perform an immediate shutdown when it sees us exiting - * unexpectedly. - * - * We treat hard shell errors such as "command not found" as fatal, too. - */ - if (wait_result_is_signal(rc, SIGTERM)) - proc_exit(1); - - ereport(wait_result_is_any_signal(rc, true) ? FATAL : DEBUG2, - (errmsg("could not restore file \"%s\" from archive: %s", - xlogfname, wait_result_to_str(rc)))); - not_available: /* @@ -281,74 +256,6 @@ not_available: return false; } -/* - * Attempt to execute an external shell command during recovery. - * - * 'command' is the shell command to be executed, 'commandName' is a - * human-readable name describing the command emitted in the logs. If - * 'failOnSignal' is true and the command is killed by a signal, a FATAL - * error is thrown. Otherwise a WARNING is emitted. - * - * This is currently used for recovery_end_command and archive_cleanup_command. - */ -void -ExecuteRecoveryCommand(const char *command, const char *commandName, - bool failOnSignal, uint32 wait_event_info) -{ - char *xlogRecoveryCmd; - char lastRestartPointFname[MAXPGPATH]; - int rc; - XLogSegNo restartSegNo; - XLogRecPtr restartRedoPtr; - TimeLineID restartTli; - - Assert(command && commandName); - - /* - * Calculate the archive file cutoff point for use during log shipping - * replication. All files earlier than this point can be deleted from the - * archive, though there is no requirement to do so. - */ - GetOldestRestartPoint(&restartRedoPtr, &restartTli); - XLByteToSeg(restartRedoPtr, restartSegNo, wal_segment_size); - XLogFileName(lastRestartPointFname, restartTli, restartSegNo, - wal_segment_size); - - /* - * construct the command to be executed - */ - xlogRecoveryCmd = replace_percent_placeholders(command, commandName, "r", lastRestartPointFname); - - ereport(DEBUG3, - (errmsg_internal("executing %s \"%s\"", commandName, command))); - - /* - * execute the constructed command - */ - fflush(NULL); - pgstat_report_wait_start(wait_event_info); - rc = system(xlogRecoveryCmd); - pgstat_report_wait_end(); - - pfree(xlogRecoveryCmd); - - if (rc != 0) - { - /* - * If the failure was due to any sort of signal, it's best to punt and - * abort recovery. See comments in RestoreArchivedFile(). - */ - ereport((failOnSignal && wait_result_is_any_signal(rc, true)) ? FATAL : WARNING, - /*------ - translator: First %s represents a postgresql.conf parameter name like - "recovery_end_command", the 2nd is the value of that parameter, the - third an already translated error message. */ - (errmsg("%s \"%s\": %s", commandName, - command, wait_result_to_str(rc)))); - } -} - - /* * A file was restored from the archive under a temporary filename (path), * and now we want to keep it. Rename it under the permanent filename in diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 29c5bec084..07213b1897 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -4209,7 +4209,8 @@ XLogFileRead(XLogSegNo segno, int emode, TimeLineID tli, if (!RestoreArchivedFile(path, xlogfname, "RECOVERYXLOG", wal_segment_size, - InRedo)) + InRedo, + ARCHIVE_TYPE_WAL_SEGMENT)) return -1; break; diff --git a/src/backend/meson.build b/src/backend/meson.build index 436c04af08..42ff1b303d 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -27,6 +27,7 @@ subdir('port') subdir('postmaster') subdir('regex') subdir('replication') +subdir('restore') subdir('rewrite') subdir('statistics') subdir('storage') diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index ef6f98ebcd..cc665ce259 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -169,8 +169,7 @@ HandleStartupProcInterrupts(void) /* * Check if we were requested to exit without finishing recovery. */ - if (shutdown_requested) - proc_exit(1); + HandleStartupProcShutdownRequests(); /* * Emergency bailout if postmaster has died. This is to avoid the @@ -194,6 +193,16 @@ HandleStartupProcInterrupts(void) ProcessLogMemoryContextInterrupt(); } +/* + * If there is a pending shutdown request, exit. + */ +void +HandleStartupProcShutdownRequests(void) +{ + if (shutdown_requested) + proc_exit(1); +} + /* -------------------------------- * signal handler routines @@ -274,8 +283,7 @@ PreRestoreCommand(void) * shutdown request received just before this. */ in_restore_command = true; - if (shutdown_requested) - proc_exit(1); + HandleStartupProcShutdownRequests(); } void diff --git a/src/backend/restore/Makefile b/src/backend/restore/Makefile new file mode 100644 index 0000000000..983d8e980f --- /dev/null +++ b/src/backend/restore/Makefile @@ -0,0 +1,18 @@ +#------------------------------------------------------------------------- +# +# Makefile-- +# Makefile for src/backend/restore +# +# IDENTIFICATION +# src/backend/restore/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/backend/restore +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + shell_restore.o + +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/restore/meson.build b/src/backend/restore/meson.build new file mode 100644 index 0000000000..e4275b3d9c --- /dev/null +++ b/src/backend/restore/meson.build @@ -0,0 +1,5 @@ +# Copyright (c) 2024, PostgreSQL Global Development Group + +backend_sources += files( + 'shell_restore.c' +) diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c new file mode 100644 index 0000000000..42291625c1 --- /dev/null +++ b/src/backend/restore/shell_restore.c @@ -0,0 +1,245 @@ +/*------------------------------------------------------------------------- + * + * shell_restore.c + * + * These restore functions use a user-specified shell command (e.g., the + * restore_command GUC). + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/restore/shell_restore.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "access/xlog.h" +#include "access/xlogrecovery.h" +#include "access/xlog_internal.h" +#include "common/archive.h" +#include "common/percentrepl.h" +#include "postmaster/startup.h" +#include "restore/shell_restore.h" +#include "storage/ipc.h" +#include "utils/wait_event.h" + +static bool shell_restore_file(const char *file, const char *path, + const char *lastRestartPointFileName); +static void ExecuteRecoveryCommand(const char *command, + const char *commandName, + bool failOnSignal, + uint32 wait_event_info, + const char *lastRestartPointFileName); + +/* + * Attempt to execute a shell-based restore command to retrieve a WAL segment. + * + * Returns true if the command has succeeded, false otherwise. + */ +bool +shell_restore_wal_segment(const char *file, const char *path, + const char *lastRestartPointFileName) +{ + return shell_restore_file(file, path, lastRestartPointFileName); +} + +/* + * Attempt to execute a shell-based restore command to retrieve a timeline + * history file. + * + * Returns true if the command has succeeded, false otherwise. + */ +bool +shell_restore_timeline_history(const char *file, const char *path) +{ + char lastRestartPointFname[MAXPGPATH]; + + XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size); + return shell_restore_file(file, path, lastRestartPointFname); +} + +/* + * Check whether restore_command is supplied. + */ +bool +shell_restore_file_configured(void) +{ + return recoveryRestoreCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based restore command to retrieve a file. + * + * Returns true if the command has succeeded, false otherwise. + */ +static bool +shell_restore_file(const char *file, const char *path, + const char *lastRestartPointFileName) +{ + char *cmd; + int rc; + + /* Build the restore command to execute */ + cmd = BuildRestoreCommand(recoveryRestoreCommand, path, file, + lastRestartPointFileName); + + ereport(DEBUG3, + (errmsg_internal("executing restore command \"%s\"", cmd))); + + fflush(NULL); + pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND); + + /* + * PreRestoreCommand() informs the SIGTERM handler for the startup process + * that it should proc_exit() right away. This is done for the duration + * of the system() call because there isn't a good way to break out while + * it is executing. Since we might call proc_exit() in a signal handler, + * it is best to put any additional logic before or after the + * PreRestoreCommand()/PostRestoreCommand() section. + */ + PreRestoreCommand(); + + /* + * Copy xlog from archival storage to XLOGDIR + */ + rc = system(cmd); + + PostRestoreCommand(); + + pgstat_report_wait_end(); + pfree(cmd); + + /* + * Remember, we rollforward UNTIL the restore fails so failure here is + * just part of the process... that makes it difficult to determine + * whether the restore failed because there isn't an archive to restore, + * or because the administrator has specified the restore program + * incorrectly. We have to assume the former. + * + * However, if the failure was due to any sort of signal, it's best to + * punt and abort recovery. (If we "return false" here, upper levels will + * assume that recovery is complete and start up the database!) It's + * essential to abort on child SIGINT and SIGQUIT, because per spec + * system() ignores SIGINT and SIGQUIT while waiting; if we see one of + * those it's a good bet we should have gotten it too. + * + * On SIGTERM, assume we have received a fast shutdown request, and exit + * cleanly. It's pure chance whether we receive the SIGTERM first, or the + * child process. If we receive it first, the signal handler will call + * proc_exit, otherwise we do it here. If we or the child process received + * SIGTERM for any other reason than a fast shutdown request, postmaster + * will perform an immediate shutdown when it sees us exiting + * unexpectedly. + * + * We treat hard shell errors such as "command not found" as fatal, too. + */ + if (rc != 0) + { + if (wait_result_is_signal(rc, SIGTERM)) + proc_exit(1); + + ereport(wait_result_is_any_signal(rc, true) ? FATAL : DEBUG2, + (errmsg("could not restore file \"%s\" from archive: %s", + file, wait_result_to_str(rc)))); + } + + return (rc == 0); +} + +/* + * Check whether archive_cleanup_command is supplied. + */ +bool +shell_archive_cleanup_configured(void) +{ + return archiveCleanupCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based archive cleanup command. + */ +void +shell_archive_cleanup(const char *lastRestartPointFileName) +{ + ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command", + false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND, + lastRestartPointFileName); +} + +/* + * Check whether recovery_end_command is supplied. + */ +bool +shell_recovery_end_configured(void) +{ + return recoveryEndCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based end-of-recovery command. + */ +void +shell_recovery_end(const char *lastRestartPointFileName) +{ + ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true, + WAIT_EVENT_RECOVERY_END_COMMAND, + lastRestartPointFileName); +} + +/* + * Attempt to execute an external shell command during recovery. + * + * 'command' is the shell command to be executed, 'commandName' is a + * human-readable name describing the command emitted in the logs. If + * 'failOnSignal' is true and the command is killed by a signal, a FATAL + * error is thrown. Otherwise a WARNING is emitted. + * + * This is currently used for recovery_end_command and archive_cleanup_command. + */ +static void +ExecuteRecoveryCommand(const char *command, const char *commandName, + bool failOnSignal, uint32 wait_event_info, + const char *lastRestartPointFileName) +{ + char *xlogRecoveryCmd; + int rc; + + Assert(command && commandName); + + /* + * construct the command to be executed + */ + xlogRecoveryCmd = replace_percent_placeholders(command, commandName, "r", + lastRestartPointFileName); + + ereport(DEBUG3, + (errmsg_internal("executing %s \"%s\"", commandName, command))); + + /* + * execute the constructed command + */ + fflush(NULL); + pgstat_report_wait_start(wait_event_info); + rc = system(xlogRecoveryCmd); + pgstat_report_wait_end(); + + pfree(xlogRecoveryCmd); + + if (rc != 0) + { + /* + * If the failure was due to any sort of signal, it's best to punt and + * abort recovery. See comments in shell_restore(). + */ + ereport((failOnSignal && wait_result_is_any_signal(rc, true)) ? FATAL : WARNING, + /*------ + translator: First %s represents a postgresql.conf parameter name like + "recovery_end_command", the 2nd is the value of that parameter, the + third an already translated error message. */ + (errmsg("%s \"%s\": %s", commandName, + command, wait_result_to_str(rc)))); + } +} diff --git a/src/include/Makefile b/src/include/Makefile index b8b576a4de..7e20f3e4c2 100644 --- a/src/include/Makefile +++ b/src/include/Makefile @@ -20,7 +20,7 @@ all: pg_config.h pg_config_ext.h pg_config_os.h SUBDIRS = access archive bootstrap catalog commands common datatype \ executor fe_utils foreign jit \ lib libpq mb nodes optimizer parser partitioning postmaster \ - regex replication rewrite \ + regex replication restore rewrite \ statistics storage tcop snowball snowball/libstemmer tsearch \ tsearch/dicts utils port port/atomics port/win32 port/win32_msvc \ port/win32_msvc/sys port/win32/arpa port/win32/netinet \ diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h index 0701475fb4..67e0fdcdec 100644 --- a/src/include/access/xlogarchive.h +++ b/src/include/access/xlogarchive.h @@ -17,9 +17,16 @@ #include "access/xlogdefs.h" +typedef enum ArchiveType +{ + ARCHIVE_TYPE_WAL_SEGMENT, + ARCHIVE_TYPE_TIMELINE_HISTORY, + ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS +} ArchiveType; + extern bool RestoreArchivedFile(char *path, const char *xlogfname, const char *recovername, off_t expectedSize, - bool cleanupEnabled); + bool cleanupEnabled, ArchiveType archive_type); extern void ExecuteRecoveryCommand(const char *command, const char *commandName, bool failOnSignal, uint32 wait_event_info); extern void KeepFileRestoredFromArchive(const char *path, const char *xlogfname); diff --git a/src/include/meson.build b/src/include/meson.build index a28f115d86..3d30cf7972 100644 --- a/src/include/meson.build +++ b/src/include/meson.build @@ -150,6 +150,7 @@ header_subdirs = [ 'postmaster', 'regex', 'replication', + 'restore', 'rewrite', 'statistics', 'storage', diff --git a/src/include/postmaster/startup.h b/src/include/postmaster/startup.h index dde7ebde88..6ba1e48c02 100644 --- a/src/include/postmaster/startup.h +++ b/src/include/postmaster/startup.h @@ -26,6 +26,7 @@ extern PGDLLIMPORT int log_startup_progress_interval; extern void HandleStartupProcInterrupts(void); +extern void HandleStartupProcShutdownRequests(void); extern void StartupProcessMain(char *startup_data, size_t startup_data_len) pg_attribute_noreturn(); extern void PreRestoreCommand(void); extern void PostRestoreCommand(void); diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h new file mode 100644 index 0000000000..28b5b7bc92 --- /dev/null +++ b/src/include/restore/shell_restore.h @@ -0,0 +1,26 @@ +/*------------------------------------------------------------------------- + * + * shell_restore.h + * Exports for restoring via shell. + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * src/include/restore/shell_restore.h + * + *------------------------------------------------------------------------- + */ +#ifndef _SHELL_RESTORE_H +#define _SHELL_RESTORE_H + +extern bool shell_restore_file_configured(void); +extern bool shell_restore_wal_segment(const char *file, const char *path, + const char *lastRestartPointFileName); +extern bool shell_restore_timeline_history(const char *file, const char *path); + +extern bool shell_archive_cleanup_configured(void); +extern void shell_archive_cleanup(const char *lastRestartPointFileName); + +extern bool shell_recovery_end_configured(void); +extern void shell_recovery_end(const char *lastRestartPointFileName); + +#endif /* _SHELL_RESTORE_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index cfa9d5aaea..a40c59b2e0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -136,6 +136,7 @@ ArchiveOpts ArchiveShutdownCB ArchiveStartupCB ArchiveStreamState +ArchiveType ArchiverOutput ArchiverStage ArrayAnalyzeExtraData -- 2.25.1 --3V7upXqbjpZ4EhLz Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v20-0003-rename-archive-modules.sgml-to-archive-and-resto.patch" From a04b5b040882b4e53dcc9ba60ce4474b1da7cd12 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 15 Feb 2023 14:45:17 -0800 Subject: [PATCH v20 3/5] rename archive-modules.sgml to archive-and-restore-modules.sgml --- .../{archive-modules.sgml => archive-and-restore-modules.sgml} | 0 doc/src/sgml/filelist.sgml | 2 +- doc/src/sgml/postgres.sgml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename doc/src/sgml/{archive-modules.sgml => archive-and-restore-modules.sgml} (100%) diff --git a/doc/src/sgml/archive-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml similarity index 100% rename from doc/src/sgml/archive-modules.sgml rename to doc/src/sgml/archive-and-restore-modules.sgml diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index e0dca81cb2..31a57943f7 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -101,7 +101,7 @@ - + diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml index 2c107199d3..23522ccaf3 100644 --- a/doc/src/sgml/postgres.sgml +++ b/doc/src/sgml/postgres.sgml @@ -228,7 +228,7 @@ break is not needed in a wider output rendering. &bgworker; &logicaldecoding; &replication-origins; - &archive-modules; + &archive-and-restore-modules; -- 2.25.1 --3V7upXqbjpZ4EhLz Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v20-0004-restructure-archive-modules-docs-in-preparation-.patch" From 726811882ed8d047da185143dc14a299b58866df Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 15 Feb 2023 14:48:36 -0800 Subject: [PATCH v20 4/5] restructure archive modules docs in preparation for restore modules --- doc/src/sgml/archive-and-restore-modules.sgml | 226 ++++++++++-------- 1 file changed, 120 insertions(+), 106 deletions(-) diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml index cf7438a759..a52d15082d 100644 --- a/doc/src/sgml/archive-and-restore-modules.sgml +++ b/doc/src/sgml/archive-and-restore-modules.sgml @@ -1,9 +1,9 @@ - + - - Archive Modules - - Archive Modules + + Archive and Restore Modules + + Archive and Restore Modules @@ -14,45 +14,52 @@ performant. - - When a custom is configured, PostgreSQL - will submit completed WAL files to the module, and the server will avoid - recycling or removing these WAL files until the module indicates that the files - were successfully archived. It is ultimately up to the module to decide what - to do with each WAL file, but many recommendations are listed at - . - - - - Archiving modules must at least consist of an initialization function (see - ) and the required callbacks (see - ). However, archive modules are - also permitted to do much more (e.g., declare GUCs and register background - workers). - - The contrib/basic_archive module contains a working example, which demonstrates some useful techniques. - - Initialization Functions - - _PG_archive_module_init + + Archive Modules + + Archive Modules + + + When a custom is configured, + PostgreSQL will submit completed WAL files to the module, and the server + will avoid recycling or removing these WAL files until the module indicates + that the files were successfully archived. It is ultimately up to the + module to decide what to do with each WAL file, but many recommendations are + listed at . + + - An archive library is loaded by dynamically loading a shared library with the - 's name as the library base name. The - normal library search path is used to locate the library. To provide the - required archive module callbacks and to indicate that the library is - actually an archive module, it needs to provide a function named - _PG_archive_module_init. The result of the function - must be a pointer to a struct of type - ArchiveModuleCallbacks, which contains everything - that the core code needs to know to make use of the archive module. The - return value needs to be of server lifetime, which is typically achieved by - defining it as a static const variable in global scope. + Archiving modules must at least consist of an initialization function (see + ) and the required callbacks (see + ). However, archive modules are + also permitted to do much more (e.g., declare GUCs and register background + workers). + + + + Initialization Functions + + _PG_archive_module_init + + + + An archive library is loaded by dynamically loading a shared library with + the 's name as the library base name. + The normal library search path is used to locate the library. To provide + the required archive module callbacks and to indicate that the library is + actually an archive module, it needs to provide a function named + _PG_archive_module_init. The result of the function + must be a pointer to a struct of type + ArchiveModuleCallbacks, which contains everything + that the core code needs to know to make use of the archive module. The + return value needs to be of server lifetime, which is typically achieved by + defining it as a static const variable in global scope. typedef struct ArchiveModuleCallbacks @@ -65,103 +72,110 @@ typedef struct ArchiveModuleCallbacks typedef const ArchiveModuleCallbacks *(*ArchiveModuleInit) (void); - Only the archive_file_cb callback is required. The - others are optional. - - + Only the archive_file_cb callback is required. The + others are optional. + + - - Archive Module Callbacks - - The archive callbacks define the actual archiving behavior of the module. - The server will call them as required to process each individual WAL file. - + + Archive Module Callbacks - - Startup Callback - The startup_cb callback is called shortly after the - module is loaded. This callback can be used to perform any additional - initialization required. If the archive module has any state, it can use - state->private_data to store it. + The archive callbacks define the actual archiving behavior of the module. + The server will call them as required to process each individual WAL file. + + + + Startup Callback + + + The startup_cb callback is called shortly after the + module is loaded. This callback can be used to perform any additional + initialization required. If the archive module has any state, it can use + state->private_data to store it. typedef void (*ArchiveStartupCB) (ArchiveModuleState *state); - - + + - - Check Callback - - The check_configured_cb callback is called to determine - whether the module is fully configured and ready to accept WAL files (e.g., - its configuration parameters are set to valid values). If no - check_configured_cb is defined, the server always - assumes the module is configured. + + Check Callback + + + The check_configured_cb callback is called to + determine whether the module is fully configured and ready to accept WAL + files (e.g., its configuration parameters are set to valid values). If no + check_configured_cb is defined, the server always + assumes the module is configured. typedef bool (*ArchiveCheckConfiguredCB) (ArchiveModuleState *state); - If true is returned, the server will proceed with - archiving the file by calling the archive_file_cb - callback. If false is returned, archiving will not - proceed, and the archiver will emit the following message to the server log: + If true is returned, the server will proceed with + archiving the file by calling the archive_file_cb + callback. If false is returned, archiving will not + proceed, and the archiver will emit the following message to the server + log: WARNING: archive_mode enabled, yet archiving is not configured - In the latter case, the server will periodically call this function, and - archiving will proceed only when it returns true. - - - - - When returning false, it may be useful to append some - additional information to the generic warning message. To do that, provide - a message to the arch_module_check_errdetail macro - before returning false. Like - errdetail(), this macro accepts a format string - followed by an optional list of arguments. The resulting string will be - emitted as the DETAIL line of the warning message. + In the latter case, the server will periodically call this function, and + archiving will proceed only when it returns true. - - - - Archive Callback - - The archive_file_cb callback is called to archive a - single WAL file. + + + When returning false, it may be useful to append some + additional information to the generic warning message. To do that, + provide a message to the arch_module_check_errdetail + macro before returning false. Like + errdetail(), this macro accepts a format string + followed by an optional list of arguments. The resulting string will be + emitted as the DETAIL line of the warning message. + + + + + + Archive Callback + + + The archive_file_cb callback is called to archive a + single WAL file. typedef bool (*ArchiveFileCB) (ArchiveModuleState *state, const char *file, const char *path); - If true is returned, the server proceeds as if the file - was successfully archived, which may include recycling or removing the - original WAL file. If false is returned, the server will - keep the original WAL file and retry archiving later. - file will contain just the file name of the WAL - file to archive, while path contains the full - path of the WAL file (including the file name). - - + If true is returned, the server proceeds as if the file + was successfully archived, which may include recycling or removing the + original WAL file. If false is returned, the server + will keep the original WAL file and retry archiving later. + file will contain just the file name of the WAL + file to archive, while path contains the full + path of the WAL file (including the file name). + + - - Shutdown Callback - - The shutdown_cb callback is called when the archiver - process exits (e.g., after an error) or the value of - changes. If no - shutdown_cb is defined, no special action is taken in - these situations. If the archive module has any state, this callback should - free it to avoid leaks. + + Shutdown Callback + + + The shutdown_cb callback is called when the archiver + process exits (e.g., after an error) or the value of + changes. If no + shutdown_cb is defined, no special action is taken in + these situations. If the archive module has any state, this callback + should free it to avoid leaks. typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state); - + + -- 2.25.1 --3V7upXqbjpZ4EhLz Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v20-0005-introduce-restore_library.patch" From 407be11af91b11a18e49c530b08a1b4758f5e0fd Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 15 Feb 2023 22:06:04 -0800 Subject: [PATCH v20 5/5] introduce restore_library --- contrib/basic_archive/Makefile | 2 + contrib/basic_archive/basic_archive.c | 153 +++++++- contrib/basic_archive/meson.build | 5 + contrib/basic_archive/t/001_restore.pl | 44 +++ doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++- doc/src/sgml/backup.sgml | 40 +- doc/src/sgml/basic-archive.sgml | 31 +- doc/src/sgml/config.sgml | 53 ++- doc/src/sgml/high-availability.sgml | 18 +- src/backend/access/transam/xlog.c | 20 +- src/backend/access/transam/xlogarchive.c | 96 ++++- src/backend/access/transam/xlogrecovery.c | 14 +- src/backend/postmaster/checkpointer.c | 54 +++ src/backend/postmaster/startup.c | 44 ++- src/backend/restore/shell_restore.c | 85 ++++- src/backend/utils/misc/guc_tables.c | 11 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/restore/restore_module.h | 143 +++++++ src/include/restore/shell_restore.h | 16 +- src/tools/pgindent/typedefs.list | 2 + 20 files changed, 1104 insertions(+), 84 deletions(-) create mode 100644 contrib/basic_archive/t/001_restore.pl create mode 100644 src/include/restore/restore_module.h diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile index 100ed81f12..e61f7d676f 100644 --- a/contrib/basic_archive/Makefile +++ b/contrib/basic_archive/Makefile @@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c # typical installcheck users do not have (e.g. buildfarm clients). NO_INSTALLCHECK = 1 +TAP_TESTS = 1 + ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c index 6b102e9072..e0d8dce364 100644 --- a/contrib/basic_archive/basic_archive.c +++ b/contrib/basic_archive/basic_archive.c @@ -17,6 +17,11 @@ * a file is successfully archived and then the system crashes before * a durable record of the success has been made. * + * This file also demonstrates a basic restore library implementation that + * is roughly equivalent to the following shell command: + * + * cp /path/to/archivedir/%f %p + * * Copyright (c) 2022-2024, PostgreSQL Global Development Group * * IDENTIFICATION @@ -33,6 +38,7 @@ #include "archive/archive_module.h" #include "common/int.h" #include "miscadmin.h" +#include "restore/restore_module.h" #include "storage/copydir.h" #include "storage/fd.h" #include "utils/guc.h" @@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path); static bool check_archive_directory(char **newval, void **extra, GucSource source); static bool compare_files(const char *file1, const char *file2); static void basic_archive_shutdown(ArchiveModuleState *state); +static bool basic_restore_configured(RestoreModuleState *state); +static bool basic_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +static bool basic_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path); +static bool basic_restore_file(const char *file, const char *path); +static bool basic_restore_timeline_history_exists(RestoreModuleState *state, + const char *file, + const char *path); static const ArchiveModuleCallbacks basic_archive_callbacks = { .startup_cb = basic_archive_startup, @@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = { .shutdown_cb = basic_archive_shutdown }; +static const RestoreModuleCallbacks basic_restore_callbacks = { + .startup_cb = NULL, + .restore_wal_segment_configured_cb = basic_restore_configured, + .restore_wal_segment_cb = basic_restore_wal_segment, + .restore_timeline_history_configured_cb = basic_restore_configured, + .restore_timeline_history_cb = basic_restore_timeline_history, + .timeline_history_exists_configured_cb = basic_restore_configured, + .timeline_history_exists_cb = basic_restore_timeline_history_exists, + .archive_cleanup_configured_cb = NULL, + .archive_cleanup_cb = NULL, + .recovery_end_configured_cb = NULL, + .recovery_end_cb = NULL, + .shutdown_cb = NULL +}; + /* * _PG_init * @@ -71,7 +102,7 @@ void _PG_init(void) { DefineCustomStringVariable("basic_archive.archive_directory", - gettext_noop("Archive file destination directory."), + gettext_noop("Archive file source/destination directory."), NULL, &archive_directory, "", @@ -93,6 +124,17 @@ _PG_archive_module_init(void) return &basic_archive_callbacks; } +/* + * _PG_restore_module_init + * + * Returns the module's restore callbacks. + */ +const RestoreModuleCallbacks * +_PG_restore_module_init(void) +{ + return &basic_restore_callbacks; +} + /* * basic_archive_startup * @@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state) pfree(data); state->private_data = NULL; } + +/* + * basic_restore_configured + * + * Checks that archive_directory is not blank. + */ +static bool +basic_restore_configured(RestoreModuleState *state) +{ + return archive_directory != NULL && archive_directory[0] != '\0'; +} + +/* + * basic_restore_wal_segment + * + * Retrieves one archived WAL segment. + */ +static bool +basic_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName) +{ + return basic_restore_file(file, path); +} + +/* + * basic_restore_timeline_history + * + * Retrieves one timeline history file. + */ +static bool +basic_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path) +{ + return basic_restore_file(file, path); +} + +/* + * basic_restore_file + * + * Retrieves one file. + */ +static bool +basic_restore_file(const char *file, const char *path) +{ + char archive_path[MAXPGPATH]; + struct stat st; + + ereport(DEBUG1, + (errmsg("restoring \"%s\" via basic_archive", + file))); + + /* + * If the file doesn't exist, return false to indicate that there are no + * more files to restore. + */ + snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file); + if (stat(archive_path, &st) != 0) + { + int elevel = (errno == ENOENT) ? DEBUG1 : ERROR; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + archive_path))); + return false; + } + + copy_file(archive_path, path); + + ereport(DEBUG1, + (errmsg("restored \"%s\" via basic_archive", + file))); + return true; +} + +/* + * basic_restore_timeline_history_exists + * + * Check whether a timeline history file is present in the archive directory. + */ +static bool +basic_restore_timeline_history_exists(RestoreModuleState *state, + const char *file, const char *path) +{ + char archive_path[MAXPGPATH]; + struct stat st; + + ereport(DEBUG1, + (errmsg("checking existence of \"%s\" via basic_archive", + file))); + + snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file); + if (stat(archive_path, &st) != 0) + { + int elevel = (errno == ENOENT) ? DEBUG1 : ERROR; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + archive_path))); + return false; + } + + ereport(DEBUG1, + (errmsg("verified existence of \"%s\" via basic_archive", + file))); + return true; +} diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build index cc2f62bf36..8e36fa7ed6 100644 --- a/contrib/basic_archive/meson.build +++ b/contrib/basic_archive/meson.build @@ -31,4 +31,9 @@ tests += { # typical installcheck users do not have (e.g. buildfarm clients). 'runningcheck': false, }, + 'tap': { + 'tests': [ + 't/001_restore.pl', + ], + }, } diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl new file mode 100644 index 0000000000..6c01f736cf --- /dev/null +++ b/contrib/basic_archive/t/001_restore.pl @@ -0,0 +1,44 @@ + +# Copyright (c) 2024, PostgreSQL Global Development Group + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# start a node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init(has_archiving => 1, allows_streaming => 1); +my $archive_dir = $node->archive_dir; +$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os; +$node->append_conf('postgresql.conf', "archive_command = ''"); +$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'"); +$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'"); +$node->start; + +# backup the node +my $backup = 'backup'; +$node->backup($backup); + +# generate some new WAL files +$node->safe_psql('postgres', "CREATE TABLE test (a INT);"); +$node->safe_psql('postgres', "SELECT pg_switch_wal();"); +$node->safe_psql('postgres', "INSERT INTO test VALUES (1);"); + +# shut down the node (this should archive all WAL files) +$node->stop; + +# restore from the backup +my $restore = PostgreSQL::Test::Cluster->new('restore'); +$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0); +$restore->append_conf('postgresql.conf', "restore_command = ''"); +$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'"); +$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'"); +$restore->start; + +# ensure post-backup WAL is replayed +my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;"); +is($result, "1", "check restore content"); + +done_testing(); diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml index a52d15082d..15be548a73 100644 --- a/doc/src/sgml/archive-and-restore-modules.sgml +++ b/doc/src/sgml/archive-and-restore-modules.sgml @@ -8,15 +8,17 @@ PostgreSQL provides infrastructure to create custom modules for continuous - archiving (see ). While archiving via - a shell command (i.e., ) is much - simpler, a custom archive module will often be considerably more robust and - performant. + archiving and recovery (see ). While a + shell command (i.e., , + ) is much simpler, a custom module will + often be considerably more robust and performant. The contrib/basic_archive module contains a working - example, which demonstrates some useful techniques. + example, which demonstrates some useful techniques. As demonstrated in + basic_archive a single module may serve as both an + archive module and a restore module. @@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state); + + + Restore Modules + + Restore Modules + + + + When a custom is configured, + PostgreSQL will use the module for restore actions. It is + ultimately up to the module to decide how to accomplish each task, + but some recommendations are listed at + . + + + + Restore modules must at least consist of an initialization function + (see ) and the required callbacks (see + ). However, restore modules are + also permitted to do much more (e.g., declare GUCs and register background + workers). + + + + Initialization Functions + + _PG_restore_module_init + + + + An restore library is loaded by dynamically loading a shared library with + the 's name as the library base name. + The normal library search path is used to locate the library. To provide + the required restore module callbacks and to indicate that the library is + actually a restore module, it needs to provide a function named + _PG_restore_module_init. The result of the function + must be a pointer to a struct of type + RestoreModuleCallbacks, which contains everything + that the core code needs to know how to make use of the restore module. + The return value needs to be of server lifetime, which is typically + achieved by defining it as a static const variable in + global scope. + + +typedef struct RestoreModuleCallbacks +{ + RestoreStartupCB startup_cb; + RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb; + RestoreWalSegmentCB restore_wal_segment_cb; + RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb; + RestoreTimelineHistoryCB restore_timeline_history_cb; + TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb; + TimelineHistoryExistsCB timeline_history_exists_cb; + ArchiveCleanupConfiguredCB archive_cleanup_configured_cb; + ArchiveCleanupCB archive_cleanup_cb; + RecoveryEndConfiguredCB recovery_end_configured_cb; + RecoveryEndCB recovery_end_cb; + RestoreShutdownCB shutdown_cb; +} RestoreModuleCallbacks; +typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void); + + + The restore_wal_segment_configured_cb, + restore_wal_segment_cb, + restore_timeline_history_configured_cb, + restore_timeline_history_cb, + timeline_history_exists_configured_cb, and + timeline_history_exists_cb callbacks are required for + archive recovery but optional for streaming replication. The others are + always optional. + + + + + Restore Module Callbacks + + + The restore callbacks define the actual behavior of the module. The server + will call them as required to execute restore actions. + + + + Startup Callback + + + The startup_cb callback is called shortly after the + module is loaded. This callback can be used to perform any additional + initialization required. If the restore module has state, it can use + state->private_data to store it. + + +typedef void (*RestoreStartupCB) (RestoreModuleState *state); + + + + + + Restore WAL Segment Configured Callback + + The restore_wal_segment_configured_cb callback is + called to determine whether the module is fully configured and ready to + accept requests to retrieve WAL files (e.g., its configuration parameters + are set to valid values). If no + restore_wal_segment_configured_cb is defined, the + server always assumes the module is not configured to retrieve WAL files. + + +typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state); + + + If true is returned, the server will retrieve WAL files + by calling the restore_wal_segment_cb callback. If + false is returned, the server will not use the + restore_wal_segment_cb callback to retrieve WAL + files. + + + + + Restore WAL Segment Callback + + The restore_wal_segment_cb callback is called to + retrieve a single archived segment of the WAL file series for archive + recovery or streaming replication. + + +typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName); + + + This callback must return true only if the file was + successfully retrieved. If the file is not available in the archives, the + callback must return false. + file will contain just the file name + of the WAL file to retrieve, while path + contains the destination's relative path (including the file name). + lastRestartPointFileName will contain the name + of the file containing the last valid restart point. That is the earliest + file that must be kept to allow a restore to be restartable, so this + information can be used to truncate the archive to just the minimum + required to support restarting from the current restore. + lastRestartPointFileName is typically only used + by warm-standby configurations (see ). Note + that if multiple standby servers are restoring from the same archive + directory, you will need to ensure that you do not delete WAL files until + they are no longer needed by any of the servers. + + + + + Restore Timeline History Configured Callback + + The restore_timeline_history_configured_cb callback + is called to determine whether the module is fully configured and ready to + accept requests to retrieve timeline history files (e.g., its + configuration parameters are set to valid values). If no + restore_timeline_history_configured_cb is defined, + the server always assumes the module is not configured to retrieve + timeline history files. + + +typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state); + + + If true is returned, the server will retrieve timeline + history files by calling the + restore_timeline_history_cb callback. If + false is returned, the server will not use the + restore_timeline_history_cb callback to retrieve + timeline history files. + + + + + Restore Timeline History Callback + + The restore_timeline_history_cb callback is called to + retrieve a single archived timeline history file for archive recovery or + streaming replication. + + +typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path); + + + This callback must return true only if the file was + successfully retrieved. If the file is not available in the archives, the + callback must return false. + file will contain just the file name + of the WAL file to retrieve, while path + contains the destination's relative path (including the file name). + + + + + Timeline History Exists Configured Callback + + The timeline_history_exists_configured_cb callback is + called to determine whether the module is fully configured and ready to + accept requests to determine whether a timeline history file exists in the + archives (e.g., its configuration parameters are set to valid values). If + no timeline_history_exists_configured_cb is defined, + the server always assumes the module is not configured to check whether + certain timeline history files exist. + + +typedef bool (*TimelineHistoryConfiguredCB) (RestoreModuleState *state); + + + If true is returned, the server will check whether + certain timeline history files are present in the archives by calling the + timeline_history_exists_cb callback. If + false is returned, the server will not use the + timeline_history_exists_cb callback to check if + timeline history files exist in the archives. + + + + + Timeline History Exists Callback + + The timeline_history_exists_cb callback is called to + check whether a timeline history file is present in the archives. + + +typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path); + + + This callback must return true only if the file is + present in the archives. If the file is not available in the archives, + the callback must return false. + file will contain just the file name + of the timeline history file. + + + + Some restore modules might not have a dedicated way to determine whether a + timeline history file exists. For example, + restore_command only tells the server how to retrieve + files. In this case, the timeline_history_exists_cb + callback should copy the file to path, which + contains the destination's relative path (including the file name), and + the restore module should set + state->timeline_history_exists_cb_copies to + true. It is recommended to set this flag in the + module's startup_cb callback. This flag tells the + server that it should verify that the file was successfully retrieved + after invoking this callback. + + + + + Archive Cleanup Configured Callback + + The archive_cleanup_configured_cb callback is called + to determine whether the module is fully configured and ready to accept + requests to remove old WAL files from the archives (e.g., its + configuration parameters are set to valid values). If no + archive_cleanup_configured_cb is defined, the server + always assumes the module is not configured to remove old WAL files. + + +typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state); + + + If true is returned, the server will remove old WAL + files by calling the archive_cleanup_cb callback. If + false is returned, the server will not use the + archive_cleanup_cb callback to remove old WAL files. + + + + + Archive Cleanup Callback + + The archive_cleanup_cb callback is called at every + restart point by the checkpointer process and is intended to provide a + mechanism for cleaning up old archived WAL files that are no longer + needed by the standby server. + + +typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName); + + + lastRestartPointFileName will contain the + name of the file that includes the last valid restart point, like in + restore_wal_segment_cb. + + + + + Recovery End Configured Callback + + The recovery_end_configured_cb callback is called to + determine whether the module is fully configured and ready to execute its + recovery_end_cb callback once at the end of recovery + (e.g., its configuration parameters are set to valid values). If no + recovery_end_configured_cb is defined, the server + always assumes the module is not configured to execute its + recovery_end_cb callback. + + +typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state); + + + If true is returned, the server will execute the + recovery_end_cb callback once at the end of recovery. + If false is returned, the server will not use the + recovery_end_cb callback at the end of recovery. + + + + + Recovery End Callback + + The recovery_end_cb callback is called once at the + end of recovery and is intended to provide a mechanism for cleanup + following the end of replication or recovery. + + +typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName); + + + lastRestartPointFileName will contain the name + of the file containing the last valid restart point, like in + restore_wal_segment_cb. + + + + + Shutdown Callback + + The shutdown_cb callback is called when a process + that has loaded the restore module exits (e.g., after an error) or the + value of changes. If no + shutdown_cb is defined, no special action is taken in + these situations. If the restore module has state, this callback should + free it to avoid leaks. + + +typedef void (*RestoreShutdownCB) (RestoreModuleState *state); + + + + + diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index b3468eea3c..e1643f92e0 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true); The key part of all this is to set up a recovery configuration that describes how you want to recover and how far the recovery should - run. The one thing that you absolutely must specify is the restore_command, + run. The one thing that you absolutely must specify is either + restore_command or a restore_library, which tells PostgreSQL how to retrieve archived - WAL file segments. Like the archive_command, this is + WAL file segments. + + + + Like the archive_library parameter, + restore_library is a shared library. Since such + libraries are written in C, creating your own may + require considerably more effort than writing a shell command. However, + restore modules can be more performance than restoring via shell, and they + will have access to many useful server resources. For more information + about creating a restore_library, see + . + + + + Like the archive_command, + restore_command is a shell command string. It can contain %f, which is replaced by the name of the desired WAL file, and %p, which is replaced by the path name to copy the WAL file to. @@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p' - It is important that the command return nonzero exit status on failure. - The command will be called requesting files that are not - present in the archive; it must return nonzero when so asked. This is not - an error condition. An exception is that if the command was terminated by + It is important that the restore_command return nonzero + exit status on failure, or, if you are using a + restore_library, that the restore callbacks return + false on failure. The command or library + will be called requesting files that are not + present in the archive; it must fail when so asked. This is not + an error condition. An exception is that if the + restore_command was terminated by a signal (other than SIGTERM, which is used as part of a database server shutdown) or an error by the shell (such as command not found), then recovery will abort and the server will not start - up. + up. Likewise, if the restore callbacks provided by the + restore_library emit an ERROR or + FATAL, recovery will abort and the server won't start. @@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p' close as possible given the available WAL segments). Therefore, a normal recovery will end with a file not found message, the exact text of the error message depending upon your choice of - restore_command. You may also see an error message + restore_command or restore_library. + You may also see an error message at the start of recovery for a file named something like 00000001.history. This is also normal and does not indicate a problem in simple recovery situations; see diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml index b4d43ced20..f3a5a502bd 100644 --- a/doc/src/sgml/basic-archive.sgml +++ b/doc/src/sgml/basic-archive.sgml @@ -8,17 +8,20 @@ - basic_archive is an example of an archive module. This - module copies completed WAL segment files to the specified directory. This - may not be especially useful, but it can serve as a starting point for - developing your own archive module. For more information about archive - modules, see . + basic_archive is an example of an archive and restore + module. This module copies completed WAL segment files to or from the + specified directory. This may not be especially useful, but it can serve as + a starting point for developing your own archive and restore modules. For + more information about archive and restore modules, see\ + . - In order to function, this module must be loaded via + For use as an archive module, this module must be loaded via , and - must be enabled. + must be enabled. For use as a restore module, this module must be loaded + via , and recovery must be enabled (see + ). @@ -34,11 +37,12 @@ - The directory where the server should copy WAL segment files. This - directory must already exist. The default is an empty string, which - effectively halts WAL archiving, but if - is enabled, the server will accumulate WAL segment files in the - expectation that a value will soon be provided. + The directory where the server should copy WAL segment files to or from. + This directory must already exist. The default is an empty string, + which, when used for archiving, effectively halts WAL archival, but if + is enabled, the server will accumulate + WAL segment files in the expectation that a value will soon be provided. + When an empty string is used for recovery, restore will fail. @@ -46,7 +50,7 @@ These parameters must be set in postgresql.conf. - Typical usage might be: + Typical usage as an archive module might be: @@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory' Notes + When basic_archive is used as an archive module, Server crashes may leave temporary files with the prefix archtemp in the archive directory. It is recommended to delete such files before restarting the server after a crash. It is safe to diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 65a6e6c408..9a2570f87f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3906,7 +3906,8 @@ include_dir 'conf.d' recovery when the end of archived WAL is reached, but will keep trying to continue recovery by connecting to the sending server as specified by the primary_conninfo setting and/or by fetching new WAL - segments using restore_command. For this mode, the + segments using restore_command or + restore_library. For this mode, the parameters from this section and are of interest. Parameters from will @@ -3934,7 +3935,8 @@ include_dir 'conf.d' The local shell command to execute to retrieve an archived segment of - the WAL file series. This parameter is required for archive recovery, + the WAL file series. Either restore_command or + is required for archive recovery, but optional for streaming replication. Any %f in the string is replaced by the name of the file to retrieve from the archive, @@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows This parameter can only be set in the postgresql.conf - file or on the server command line. + file or on the server command line. It is only used if + restore_library is set to an empty string. If both + restore_command and + restore_library are set, an error will be raised. + + + + + + restore_library (string) + + restore_library configuration parameter + + + + + The library to use for restore actions, including retrieving archived + files and executing tasks at restartpoints and at recovery end. Either + or + restore_library is required for archive recovery, + but optional for streaming replication. If this parameter is set to an + empty string (the default), restoring via shell is enabled, and + restore_command, + archive_cleanup_command, and + recovery_end_command are used. If both + restore_library and any of + restore_command, + archive_cleanup_command, or + recovery_end_command are set, an error will be + raised. Otherwise, the specified shared library is used for restoring. + For more information, see . + + + + This parameter can only be set in the + postgresql.conf file or on the server command line. @@ -4014,7 +4051,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows This parameter can only be set in the postgresql.conf - file or on the server command line. + file or on the server command line. It is only used if + restore_library is set to an empty string. If both + archive_cleanup_command and + restore_library are set, an error will be raised. @@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows This parameter can only be set in the postgresql.conf - file or on the server command line. + file or on the server command line. It is only used if + restore_library is set to an empty string. If both + recovery_end_command and + restore_library are set, an error will be raised. diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml index b48209fc2f..a86a9fe93a 100644 --- a/doc/src/sgml/high-availability.sgml +++ b/doc/src/sgml/high-availability.sgml @@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order. In standby mode, the server continuously applies WAL received from the primary server. The standby server can read WAL from a WAL archive - (see ) or directly from the primary + (see and + ) or directly from the primary over a TCP connection (streaming replication). The standby server will also attempt to restore any WAL found in the standby cluster's pg_wal directory. That typically happens after a server @@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order. At startup, the standby begins by restoring all WAL available in the - archive location, calling restore_command. Once it + archive location, either by calling restore_command or + by executing the restore_library's callbacks. Once it reaches the end of WAL available there and restore_command + or restore_library fails, it tries to restore any WAL available in the pg_wal directory. If that fails, and streaming replication has been configured, the standby tries to connect to the primary server and start streaming WAL @@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order. server (see ). Create a file standby.signalstandby.signal in the standby's cluster data - directory. Set to a simple command to copy files from + directory. Set or + to copy files from the WAL archive. If you plan to have multiple standby servers for high availability purposes, make sure that recovery_target_timeline is set to latest (the default), to make the standby server follow the timeline change @@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order. - should return immediately + and + should return immediately if the file does not exist; the server will retry the command again if necessary. @@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order. If you're using a WAL archive, its size can be minimized using the parameter to remove files that are no - longer required by the standby server. + linkend="guc-archive-cleanup-command"/> parameter or the + 's archive cleanup callbacks to remove + files that are no longer required by the standby server. The pg_archivecleanup utility is designed specifically to be used with archive_cleanup_command in typical single-standby configurations, see . diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index e5d12f1d15..34a21c40a5 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -83,7 +83,7 @@ #include "replication/snapbuild.h" #include "replication/walreceiver.h" #include "replication/walsender.h" -#include "restore/shell_restore.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog, TimeLineID newTLI) { /* - * Execute the recovery_end_command, if any. + * Execute the recovery-end callback, if any. * - * The command is provided the archive file cutoff point for use during + * The callback is provided the archive file cutoff point for use during * log shipping replication. All files earlier than this point can be * deleted from the archive, though there is no requirement to do so. */ - if (shell_recovery_end_configured()) + if (recovery_end_configured()) { char lastRestartPointFname[MAXFNAMELEN]; GetOldestRestartPointFileName(lastRestartPointFname); - shell_recovery_end(lastRestartPointFname); + RestoreCallbacks->recovery_end_cb(restore_module_state, + lastRestartPointFname); } /* @@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags) timestamptz_to_str(xtime)) : 0)); /* - * Finally, execute archive_cleanup_command, if any. + * Finally, execute archive-cleanup callback, if any. * - * The command is provided the archive file cutoff point for use during + * The callback is provided the archive file cutoff point for use during * log shipping replication. All files earlier than this point can be * deleted from the archive, though there is no requirement to do so. */ - if (shell_archive_cleanup_configured()) + if (archive_cleanup_configured()) { char lastRestartPointFname[MAXFNAMELEN]; GetOldestRestartPointFileName(lastRestartPointFname); - shell_archive_cleanup(lastRestartPointFname); + RestoreCallbacks->archive_cleanup_cb(restore_module_state, + lastRestartPointFname); } return true; diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index 5756f52124..3a05581495 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -23,14 +23,21 @@ #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "common/archive.h" +#include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/pgarch.h" #include "postmaster/startup.h" #include "replication/walsender.h" +#include "restore/restore_module.h" #include "restore/shell_restore.h" #include "storage/fd.h" #include "storage/ipc.h" +#include "utils/memutils.h" + +char *restoreLibrary = ""; +const RestoreModuleCallbacks *RestoreCallbacks = NULL; +RestoreModuleState *restore_module_state; /* * Attempt to retrieve the specified file from off-line archival storage. @@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname, switch (archive_type) { case ARCHIVE_TYPE_WAL_SEGMENT: - if (!shell_restore_file_configured()) + if (!restore_wal_segment_configured()) goto not_available; break; case ARCHIVE_TYPE_TIMELINE_HISTORY: - if (!shell_restore_file_configured()) + if (!restore_timeline_history_configured()) goto not_available; break; case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: - if (!shell_restore_file_configured()) + if (!timeline_history_exists_configured()) goto not_available; break; } @@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname, switch (archive_type) { case ARCHIVE_TYPE_WAL_SEGMENT: - ret = shell_restore_wal_segment(xlogfname, xlogpath, - lastRestartPointFname); + ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state, + xlogfname, xlogpath, + lastRestartPointFname); break; case ARCHIVE_TYPE_TIMELINE_HISTORY: - ret = shell_restore_timeline_history(xlogfname, xlogpath); + ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state, + xlogfname, xlogpath); break; case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: - ret = shell_restore_timeline_history(xlogfname, xlogpath); + ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state, + xlogfname, xlogpath); break; } @@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname, if (ret) { + /* + * Some restore functions might not copy the file (e.g., checking + * whether a timeline history file exists), but they can set a flag to + * tell us if they do. We only need to verify file existence if this + * flag is enabled. + */ + if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS && + !restore_module_state->timeline_history_exists_cb_copies) + return true; + /* * command apparently succeeded, but let's make sure the file is * really there now and has the correct size. @@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog) unlink(archiveStatusPath); /* should we complain about failure? */ } + +/* + * Loads all the restore callbacks into our global RestoreCallbacks. The + * caller is responsible for validating the combination of library/command + * parameters that are set (e.g., restore_command and restore_library cannot + * both be set). + */ +void +LoadRestoreCallbacks(void) +{ + RestoreModuleInit init; + + /* + * If the shell command is enabled, use our special initialization + * function. Otherwise, load the library and call its + * _PG_restore_module_init(). + */ + if (restoreLibrary[0] == '\0') + init = shell_restore_init; + else + init = (RestoreModuleInit) + load_external_function(restoreLibrary, "_PG_restore_module_init", + false, NULL); + + if (init == NULL) + ereport(ERROR, + (errmsg("restore modules have to define the symbol " + "_PG_restore_module_init"))); + + RestoreCallbacks = (*init) (); + + /* restore state should be freed before calling this function */ + Assert(restore_module_state == NULL); + restore_module_state = (RestoreModuleState *) + MemoryContextAllocZero(TopMemoryContext, + sizeof(RestoreModuleState)); + + if (RestoreCallbacks->startup_cb != NULL) + RestoreCallbacks->startup_cb(restore_module_state); +} + +/* + * Call the shutdown callback of the loaded restore module, if defined. Also, + * free the restore module state if it was allocated. + * + * Processes that load restore libraries should register this via + * before_shmem_exit(). + */ +void +call_restore_module_shutdown_cb(int code, Datum arg) +{ + if (RestoreCallbacks != NULL && + RestoreCallbacks->shutdown_cb != NULL && + restore_module_state != NULL) + RestoreCallbacks->shutdown_cb(restore_module_state); + + if (restore_module_state != NULL) + { + pfree(restore_module_state); + restore_module_state = NULL; + } +} diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 07213b1897..1bf0d0ea11 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -51,6 +51,7 @@ #include "replication/slot.h" #include "replication/slotsync.h" #include "replication/walreceiver.h" +#include "restore/restore_module.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/latch.h" @@ -1117,18 +1118,21 @@ validateRecoveryParameters(void) if (StandbyModeRequested) { if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) && - (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)) + (!restore_wal_segment_configured() || + !restore_timeline_history_configured() || + !timeline_history_exists_configured())) ereport(WARNING, - (errmsg("specified neither primary_conninfo nor restore_command"), + (errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"), errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there."))); } else { - if (recoveryRestoreCommand == NULL || - strcmp(recoveryRestoreCommand, "") == 0) + if (!restore_wal_segment_configured() || + !restore_timeline_history_configured() || + !timeline_history_exists_configured()) ereport(FATAL, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("must specify restore_command when standby mode is not enabled"))); + errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled"))); } /* diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 8ef600ae72..c31d0f3bd2 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -46,6 +46,7 @@ #include "postmaster/bgwriter.h" #include "postmaster/interrupt.h" #include "replication/syncrep.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/condition_variable.h" #include "storage/fd.h" @@ -230,6 +231,25 @@ CheckpointerMain(char *startup_data, size_t startup_data_len) ALLOCSET_DEFAULT_SIZES); MemoryContextSwitchTo(checkpointer_context); + /* + * Load restore_library so that we can use its archive-cleanup callback, + * if one is defined. + * + * We also take this opportunity to set up the before_shmem_exit hook for + * the shutdown callback and to check that only one of restore_library, + * archive_cleanup_command is set. Note that we emit a WARNING upon + * detecting misconfigured parameters, and we clear the callbacks so that + * the archive-cleanup functionality is skipped. We judge this + * functionality to not be important enough to require blocking + * checkpoints or shutting down the server when the parameters are + * misconfigured. + */ + before_shmem_exit(call_restore_module_shutdown_cb, 0); + if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + archiveCleanupCommand, "archive_cleanup_command", + WARNING)) + LoadRestoreCallbacks(); + /* * If an exception is encountered, processing resumes here. * @@ -562,6 +582,10 @@ HandleCheckpointerInterrupts(void) if (ConfigReloadPending) { + bool archive_cleanup_settings_changed = false; + char *prevRestoreLibrary = pstrdup(restoreLibrary); + char *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand); + ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP); @@ -577,6 +601,36 @@ HandleCheckpointerInterrupts(void) * because of SIGHUP. */ UpdateSharedMemoryConfig(); + + /* + * If the archive-cleanup settings have changed, call the currently + * loaded shutdown callback and clear all the restore callbacks. + * There's presently no good way to unload a library besides + * restarting the process, and there should be little harm in leaving + * it around, so we just leave it loaded. + */ + if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 || + strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0) + { + archive_cleanup_settings_changed = true; + call_restore_module_shutdown_cb(0, (Datum) 0); + RestoreCallbacks = NULL; + } + + /* + * As in CheckpointerMain(), we only emit a WARNING if we detect that + * both restore_library and archive_cleanup_command are set. We do + * this even if the archive-cleanup settings haven't changed to remind + * the user about the misconfiguration. + */ + if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + archiveCleanupCommand, "archive_cleanup_command", + WARNING) && + archive_cleanup_settings_changed) + LoadRestoreCallbacks(); + + pfree(prevRestoreLibrary); + pfree(prevArchiveCleanupCommand); } if (ShutdownRequestPending) { diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cc665ce259..f8d3d6b3d2 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -26,6 +26,7 @@ #include "miscadmin.h" #include "postmaster/auxprocess.h" #include "postmaster/startup.h" +#include "restore/restore_module.h" #include "storage/ipc.h" #include "storage/pmsignal.h" #include "storage/procsignal.h" @@ -119,13 +120,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS) * Re-read the config file. * * If one of the critical walreceiver options has changed, flag xlog.c - * to restart it. + * to restart it. Also, check for invalid combinations of the command/library + * parameters and reload the restore callbacks if necessary. */ static void StartupRereadConfig(void) { char *conninfo = pstrdup(PrimaryConnInfo); char *slotname = pstrdup(PrimarySlotName); + char *prevRestoreLibrary = pstrdup(restoreLibrary); + char *prevRestoreCommand = pstrdup(recoveryRestoreCommand); + char *prevRecoveryEndCommand = pstrdup(recoveryEndCommand); bool tempSlot = wal_receiver_create_temp_slot; bool conninfoChanged; bool slotnameChanged; @@ -147,6 +152,30 @@ StartupRereadConfig(void) if (conninfoChanged || slotnameChanged || tempSlotChanged) StartupRequestWalReceiverRestart(); + + /* + * If the restore settings have changed, call the currently loaded + * shutdown callback and load the new callbacks. There's presently no + * good way to unload a library besides restarting the process, and there + * should be little harm in leaving it around, so we just leave it loaded. + */ + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryRestoreCommand, "restore_command", + ERROR); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryEndCommand, "recovery_end_command", + ERROR); + if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 || + strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 || + strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0) + { + call_restore_module_shutdown_cb(0, (Datum) 0); + LoadRestoreCallbacks(); + } + + pfree(prevRestoreLibrary); + pfree(prevRestoreCommand); + pfree(prevRecoveryEndCommand); } /* Handle various signals that might be sent to the startup process */ @@ -261,6 +290,19 @@ StartupProcessMain(char *startup_data, size_t startup_data_len) */ sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); + /* + * Check for invalid combinations of the command/library parameters and + * load the callbacks. + */ + before_shmem_exit(call_restore_module_shutdown_cb, 0); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryRestoreCommand, "restore_command", + ERROR); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryEndCommand, "recovery_end_command", + ERROR); + LoadRestoreCallbacks(); + /* * Do what we came for. */ diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c index 42291625c1..be25f04044 100644 --- a/src/backend/restore/shell_restore.c +++ b/src/backend/restore/shell_restore.c @@ -3,7 +3,8 @@ * shell_restore.c * * These restore functions use a user-specified shell command (e.g., the - * restore_command GUC). + * restore_command GUC). It is used as the default, but other modules may + * define their own restore logic. * * Copyright (c) 2024, PostgreSQL Global Development Group * @@ -22,25 +23,76 @@ #include "common/archive.h" #include "common/percentrepl.h" #include "postmaster/startup.h" +#include "restore/restore_module.h" #include "restore/shell_restore.h" #include "storage/ipc.h" #include "utils/wait_event.h" +static void shell_restore_startup(RestoreModuleState *state); +static bool shell_restore_file_configured(RestoreModuleState *state); static bool shell_restore_file(const char *file, const char *path, const char *lastRestartPointFileName); +static bool shell_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +static bool shell_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path); +static bool shell_archive_cleanup_configured(RestoreModuleState *state); +static void shell_archive_cleanup(RestoreModuleState *state, + const char *lastRestartPointFileName); +static bool shell_recovery_end_configured(RestoreModuleState *state); +static void shell_recovery_end(RestoreModuleState *state, + const char *lastRestartPointFileName); static void ExecuteRecoveryCommand(const char *command, const char *commandName, bool failOnSignal, uint32 wait_event_info, const char *lastRestartPointFileName); +static const RestoreModuleCallbacks shell_restore_callbacks = { + .startup_cb = shell_restore_startup, + .restore_wal_segment_configured_cb = shell_restore_file_configured, + .restore_wal_segment_cb = shell_restore_wal_segment, + .restore_timeline_history_configured_cb = shell_restore_file_configured, + .restore_timeline_history_cb = shell_restore_timeline_history, + .timeline_history_exists_configured_cb = shell_restore_file_configured, + .timeline_history_exists_cb = shell_restore_timeline_history, + .archive_cleanup_configured_cb = shell_archive_cleanup_configured, + .archive_cleanup_cb = shell_archive_cleanup, + .recovery_end_configured_cb = shell_recovery_end_configured, + .recovery_end_cb = shell_recovery_end, + .shutdown_cb = NULL +}; + +/* + * shell_restore_init + * + * Returns the callbacks for restoring via shell. + */ +const RestoreModuleCallbacks * +shell_restore_init(void) +{ + return &shell_restore_callbacks; +} + +/* + * Indicates that our timeline_history_exists_cb only attempts to retrieve the + * file, so the caller should verify the file exists afterwards. + */ +static void +shell_restore_startup(RestoreModuleState *state) +{ + state->timeline_history_exists_cb_copies = true; +} + /* * Attempt to execute a shell-based restore command to retrieve a WAL segment. * * Returns true if the command has succeeded, false otherwise. */ -bool -shell_restore_wal_segment(const char *file, const char *path, +static bool +shell_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, const char *lastRestartPointFileName) { return shell_restore_file(file, path, lastRestartPointFileName); @@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path, * * Returns true if the command has succeeded, false otherwise. */ -bool -shell_restore_timeline_history(const char *file, const char *path) +static bool +shell_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path) { char lastRestartPointFname[MAXPGPATH]; @@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path) /* * Check whether restore_command is supplied. */ -bool -shell_restore_file_configured(void) +static bool +shell_restore_file_configured(RestoreModuleState *state) { return recoveryRestoreCommand[0] != '\0'; } @@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path, /* * Check whether archive_cleanup_command is supplied. */ -bool -shell_archive_cleanup_configured(void) +static bool +shell_archive_cleanup_configured(RestoreModuleState *state) { return archiveCleanupCommand[0] != '\0'; } @@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void) /* * Attempt to execute a shell-based archive cleanup command. */ -void -shell_archive_cleanup(const char *lastRestartPointFileName) +static void +shell_archive_cleanup(RestoreModuleState *state, + const char *lastRestartPointFileName) { ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command", false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND, @@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName) /* * Check whether recovery_end_command is supplied. */ -bool -shell_recovery_end_configured(void) +static bool +shell_recovery_end_configured(RestoreModuleState *state) { return recoveryEndCommand[0] != '\0'; } @@ -181,8 +235,9 @@ shell_recovery_end_configured(void) /* * Attempt to execute a shell-based end-of-recovery command. */ -void -shell_recovery_end(const char *lastRestartPointFileName) +static void +shell_recovery_end(RestoreModuleState *state, + const char *lastRestartPointFileName) { ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true, WAIT_EVENT_RECOVERY_END_COMMAND, diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 1e71e7db4a..f84724a0ed 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -70,6 +70,7 @@ #include "replication/slot.h" #include "replication/slotsync.h" #include "replication/syncrep.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/large_object.h" #include "storage/pg_shmem.h" @@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, + gettext_noop("Sets the library that will be called for restore actions."), + NULL + }, + &restoreLibrary, + "", + NULL, NULL, NULL + }, + { {"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, gettext_noop("Sets the shell command that will be executed at every restart point."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 2244ee52f7..5265857519 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -284,6 +284,7 @@ # placeholders: %p = path of file to restore # %f = file name only # e.g. 'cp /mnt/server/archivedir/%f %p' +#restore_library = '' # library to use for restore actions #archive_cleanup_command = '' # command to execute at every restartpoint #recovery_end_command = '' # command to execute at completion of recovery diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h new file mode 100644 index 0000000000..cc148e855a --- /dev/null +++ b/src/include/restore/restore_module.h @@ -0,0 +1,143 @@ +/*------------------------------------------------------------------------- + * + * restore_module.h + * Exports for restore modules. + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * src/include/restore/restore_module.h + * + *------------------------------------------------------------------------- + */ +#ifndef _RESTORE_MODULE_H +#define _RESTORE_MODULE_H + +/* + * The value of the restore_library GUC. + */ +extern PGDLLIMPORT char *restoreLibrary; + +typedef struct RestoreModuleState +{ + /* + * Private data pointer for use by a restore module. This can be used to + * store state for the module that will be passed to each of its + * callbacks. + */ + void *private_data; + + /* + * Indicates whether the callback that checks for timeline existence + * merely copies the file. If set to true, the server will verify that + * the timeline history file exists after the callback returns. + */ + bool timeline_history_exists_cb_copies; +} RestoreModuleState; + +/* + * Restore module callbacks + * + * These callback functions should be defined by restore libraries and returned + * via _PG_restore_module_init(). For more information about the purpose of + * each callback, refer to the restore modules documentation. + */ +typedef void (*RestoreStartupCB) (RestoreModuleState *state); +typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state); +typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state); +typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, + const char *file, const char *path); +typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state); +typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, + const char *file, const char *path); +typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state); +typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, + const char *lastRestartPointFileName); +typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state); +typedef void (*RecoveryEndCB) (RestoreModuleState *state, + const char *lastRestartPointFileName); +typedef void (*RestoreShutdownCB) (RestoreModuleState *state); + +typedef struct RestoreModuleCallbacks +{ + RestoreStartupCB startup_cb; + RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb; + RestoreWalSegmentCB restore_wal_segment_cb; + RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb; + RestoreTimelineHistoryCB restore_timeline_history_cb; + TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb; + TimelineHistoryExistsCB timeline_history_exists_cb; + ArchiveCleanupConfiguredCB archive_cleanup_configured_cb; + ArchiveCleanupCB archive_cleanup_cb; + RecoveryEndConfiguredCB recovery_end_configured_cb; + RecoveryEndCB recovery_end_cb; + RestoreShutdownCB shutdown_cb; +} RestoreModuleCallbacks; + +/* + * Type of the shared library symbol _PG_restore_module_init that is looked up + * when loading a restore library. + */ +typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void); + +extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void); + +extern void LoadRestoreCallbacks(void); +extern void call_restore_module_shutdown_cb(int code, Datum arg); + +extern const RestoreModuleCallbacks *RestoreCallbacks; +extern RestoreModuleState *restore_module_state; + +/* + * The following *_configured() functions are convenient wrappers around the + * *_configured_cb() callback functions with additional defensive NULL checks. + */ + +static inline bool +restore_wal_segment_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->restore_wal_segment_configured_cb != NULL && + RestoreCallbacks->restore_wal_segment_cb != NULL && + RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state); +} + +static inline bool +restore_timeline_history_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->restore_timeline_history_configured_cb != NULL && + RestoreCallbacks->restore_timeline_history_cb != NULL && + RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state); +} + +static inline bool +timeline_history_exists_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->timeline_history_exists_configured_cb != NULL && + RestoreCallbacks->timeline_history_exists_cb != NULL && + RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state); +} + +static inline bool +archive_cleanup_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->archive_cleanup_configured_cb != NULL && + RestoreCallbacks->archive_cleanup_cb != NULL && + RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state); +} + +static inline bool +recovery_end_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->recovery_end_configured_cb != NULL && + RestoreCallbacks->recovery_end_cb != NULL && + RestoreCallbacks->recovery_end_configured_cb(restore_module_state); +} + +#endif /* _RESTORE_MODULE_H */ diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h index 28b5b7bc92..6352623866 100644 --- a/src/include/restore/shell_restore.h +++ b/src/include/restore/shell_restore.h @@ -12,15 +12,13 @@ #ifndef _SHELL_RESTORE_H #define _SHELL_RESTORE_H -extern bool shell_restore_file_configured(void); -extern bool shell_restore_wal_segment(const char *file, const char *path, - const char *lastRestartPointFileName); -extern bool shell_restore_timeline_history(const char *file, const char *path); +#include "restore/restore_module.h" -extern bool shell_archive_cleanup_configured(void); -extern void shell_archive_cleanup(const char *lastRestartPointFileName); - -extern bool shell_recovery_end_configured(void); -extern void shell_recovery_end(const char *lastRestartPointFileName); +/* + * Since the logic for restoring via shell commands is in the core server and + * does not need to be loaded via a shared library, it has a special + * initialization function. + */ +extern const RestoreModuleCallbacks *shell_restore_init(void); #endif /* _SHELL_RESTORE_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a40c59b2e0..db4cc68a88 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2405,6 +2405,8 @@ ResourceOwnerDesc ResourceReleaseCallback ResourceReleaseCallbackItem ResourceReleasePhase +RestoreModuleCallbacks +RestoreModuleState RestoreOptions RestorePass RestrictInfo -- 2.25.1 --3V7upXqbjpZ4EhLz--