Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ SUBDIRS = \
pg_prewarm \
pg_stat_statements \
pg_surgery \
pg_target_promote \
pg_trgm \
pgrowlocks \
pgstattuple \
Expand Down
1 change: 1 addition & 0 deletions contrib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ subdir('pgrowlocks')
subdir('pg_stat_statements')
subdir('pgstattuple')
subdir('pg_surgery')
subdir('pg_target_promote')
subdir('pg_trgm')
subdir('pg_visibility')
subdir('pg_walinspect')
Expand Down
21 changes: 21 additions & 0 deletions contrib/pg_target_promote/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# contrib/pg_target_promote/Makefile

MODULE_big = pg_target_promote
OBJS = \
$(WIN32RES) \
pg_target_promote.o

EXTENSION = pg_target_promote
DATA = pg_target_promote--1.0.sql
PGFILEDESC = "pg_target_promote - promote standby to a target timeline"

ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
else
subdir = contrib/pg_target_promote
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
include $(top_srcdir)/contrib/contrib-global.mk
endif
23 changes: 23 additions & 0 deletions contrib/pg_target_promote/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2025-2025, PostgreSQL Global Development Group

pg_target_promote_sources = files(
'pg_target_promote.c',
)

if host_system == 'windows'
pg_target_promote_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
'--NAME', 'pg_target_promote',
'--FILEDESC', 'pg_target_promote - promote standby to a target timeline',])
endif

pg_target_promote = shared_module('pg_target_promote',
pg_target_promote_sources,
kwargs: contrib_mod_args,
)
contrib_targets += pg_target_promote

install_data(
'pg_target_promote--1.0.sql',
'pg_target_promote.control',
kwargs: contrib_data_args,
)
13 changes: 13 additions & 0 deletions contrib/pg_target_promote/pg_target_promote--1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* contrib/pg_target_promote/pg_target_promote--1.0.sql */

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION pg_target_promote" to load this file. \quit

CREATE FUNCTION pg_target_promote(target_timeline integer,
wait boolean DEFAULT true,
wait_seconds integer DEFAULT 60)
RETURNS boolean
AS 'MODULE_PATHNAME', 'pg_target_promote_proxy'
LANGUAGE C STRICT VOLATILE PARALLEL SAFE;

REVOKE EXECUTE ON FUNCTION pg_target_promote(integer, boolean, integer) FROM public;
26 changes: 26 additions & 0 deletions contrib/pg_target_promote/pg_target_promote.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* contrib/pg_target_promote/pg_target_promote.c */

#include "postgres.h"

#include "access/xlog.h"
#include "fmgr.h"

/*
* pg_target_promote() is implemented in the core backend
* (src/backend/access/transam/xlogfuncs.c). This extension provides
* a thin proxy so that the SQL function is only available after
* CREATE EXTENSION, without modifying the core pg_proc catalog.
*/

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(pg_target_promote_proxy);

Datum
pg_target_promote_proxy(PG_FUNCTION_ARGS)
{
return DirectFunctionCall3(pg_target_promote,
PG_GETARG_DATUM(0),
PG_GETARG_DATUM(1),
PG_GETARG_DATUM(2));
}
5 changes: 5 additions & 0 deletions contrib/pg_target_promote/pg_target_promote.control
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# contrib/pg_target_promote/pg_target_promote.control
comment = 'promote standby server to a target timeline'
default_version = '1.0'
module_pathname = '$libdir/pg_target_promote'
relocatable = true
22 changes: 21 additions & 1 deletion src/backend/access/transam/xlog.c
Original file line number Diff line number Diff line change
Expand Up @@ -5989,7 +5989,27 @@ StartupXLOG(void)
newTLI = endOfRecoveryInfo->lastRecTLI;
if (ArchiveRecoveryRequested)
{
newTLI = findNewestTimeLine(recoveryTargetTLI) + 1;
/*
* If pg_target_promote() requested a specific timeline, use it.
* Otherwise, pick the next available timeline ID automatically.
*/
if (promoteTargetTLI != 0)
{
newTLI = promoteTargetTLI;

/*
* The requested timeline must not already exist; otherwise we
* would risk conflicting with existing WAL on that timeline.
*/
if (existsTimeLineHistory(newTLI))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("target timeline %u already exists",
newTLI)));
}
else
newTLI = findNewestTimeLine(recoveryTargetTLI) + 1;

ereport(LOG,
(errmsg("selected new timeline ID: %u", newTLI)));

Expand Down
156 changes: 156 additions & 0 deletions src/backend/access/transam/xlogfuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <unistd.h>

#include "access/htup_details.h"
#include "access/timeline.h"
#include "access/xlog_internal.h"
#include "access/xlogbackup.h"
#include "access/xlogrecovery.h"
Expand Down Expand Up @@ -748,3 +749,158 @@ pg_promote(PG_FUNCTION_ARGS)
wait_seconds)));
PG_RETURN_BOOL(false);
}

/*
* Promotes a standby server, switching to a specific target timeline.
*
* This is like pg_promote(), but the caller specifies the timeline ID to
* switch to, rather than letting the server choose the next available one.
* A result of "true" means that promotion has been completed if "wait" is
* "true", or initiated if "wait" is false.
*/
Datum
pg_target_promote(PG_FUNCTION_ARGS)
{
TimeLineID target_tli = PG_GETARG_INT32(0);
bool wait = PG_GETARG_BOOL(1);
int wait_seconds = PG_GETARG_INT32(2);
FILE *promote_file;
FILE *target_file;
char tli_buf[32];
int tli_len;
int i;

if (!RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("recovery is not in progress"),
errhint("Recovery control functions can only be executed during recovery.")));

if (wait_seconds <= 0)
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("\"wait_seconds\" must not be negative or zero")));

if (target_tli == 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("\"target_timeline\" must be greater than 0")));

{
TimeLineID current_tli;

GetXLogReplayRecPtr(&current_tli);

if (target_tli < current_tli)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("target timeline %u must be greater than or equal to current timeline %u",
target_tli, current_tli)));
}

/*
* The requested timeline must not already exist; otherwise we would risk
* conflicting with existing WAL on that timeline.
*/
if (existsTimeLineHistory(target_tli))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("timeline %u already exists", target_tli)));

/* create the promote signal file */
promote_file = AllocateFile(PROMOTE_SIGNAL_FILE, "w");
if (!promote_file)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not create file \"%s\": %m",
PROMOTE_SIGNAL_FILE)));

if (FreeFile(promote_file))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write file \"%s\": %m",
PROMOTE_SIGNAL_FILE)));

/* create the target timeline signal file */
target_file = AllocateFile(PROMOTE_TARGET_SIGNAL_FILE, "w");
if (!target_file)
{
(void) unlink(PROMOTE_SIGNAL_FILE);
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not create file \"%s\": %m",
PROMOTE_TARGET_SIGNAL_FILE)));
}

tli_len = snprintf(tli_buf, sizeof(tli_buf), "%u\n", target_tli);

if (fwrite(tli_buf, 1, tli_len, target_file) != (size_t) tli_len)
{
(void) FreeFile(target_file);
(void) unlink(PROMOTE_SIGNAL_FILE);
(void) unlink(PROMOTE_TARGET_SIGNAL_FILE);
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write file \"%s\": %m",
PROMOTE_TARGET_SIGNAL_FILE)));
}

if (FreeFile(target_file))
{
(void) unlink(PROMOTE_SIGNAL_FILE);
(void) unlink(PROMOTE_TARGET_SIGNAL_FILE);
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write file \"%s\": %m",
PROMOTE_TARGET_SIGNAL_FILE)));
}

/* signal the postmaster */
if (kill(PostmasterPid, SIGUSR1) != 0)
{
(void) unlink(PROMOTE_SIGNAL_FILE);
(void) unlink(PROMOTE_TARGET_SIGNAL_FILE);
ereport(ERROR,
(errcode(ERRCODE_SYSTEM_ERROR),
errmsg("failed to send signal to postmaster: %m")));
}

/* return immediately if waiting was not requested */
if (!wait)
PG_RETURN_BOOL(true);

/* wait for the amount of time wanted until promotion */
for (i = 0; i < WAITS_PER_SECOND * wait_seconds; i++)
{
int rc;

ResetLatch(MyLatch);

if (!RecoveryInProgress())
PG_RETURN_BOOL(true);

CHECK_FOR_INTERRUPTS();

rc = WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
1000L / WAITS_PER_SECOND,
WAIT_EVENT_PROMOTE);

/*
* Emergency bailout if postmaster has died. This is to avoid the
* necessity for manual cleanup of all postmaster children.
*/
if (rc & WL_POSTMASTER_DEATH)
ereport(FATAL,
(errcode(ERRCODE_ADMIN_SHUTDOWN),
errmsg("terminating connection due to unexpected postmaster exit"),
errcontext("while waiting on promotion")));
}

ereport(WARNING,
(errmsg_plural("server did not promote within %d second",
"server did not promote within %d seconds",
wait_seconds,
wait_seconds)));
PG_RETURN_BOOL(false);
}
Loading
Loading