#include "storage/subsystems.h"
#include "utils/datetime.h"
#include "utils/fmgrprotos.h"
+#include "utils/guc.h"
#include "utils/guc_hooks.h"
#include "utils/pgstat_internal.h"
#include "utils/pg_lsn.h"
TransactionId recoveryTargetXid;
char *recovery_target_time_string;
TimestampTz recoveryTargetTime;
-const char *recoveryTargetName;
+char *recoveryTargetName;
XLogRecPtr recoveryTargetLSN;
int recovery_min_apply_delay = 0;
static void SetCurrentChunkStartTime(TimestampTz xtime);
static void SetLatestXTime(TimestampTz xtime);
+static RecoveryTargetType DetermineRecoveryTargetType(void);
/*
* Register shared memory for WAL recovery
static void
validateRecoveryParameters(void)
{
+ /* Reject conflicting targets even when recovery was not requested */
+ recoveryTarget = DetermineRecoveryTargetType();
+
if (!ArchiveRecoveryRequested)
return;
}
/*
- * Recovery target settings: Only one of the several recovery_target* settings
- * may be set. Setting a second one results in an error. The global variable
- * recoveryTarget tracks which kind of recovery target was chosen. Other
- * variables store the actual target value (for example a string or a xid).
- * The assign functions of the parameters check whether a competing parameter
- * was already set. But we want to allow setting the same parameter multiple
- * times. We also want to allow unsetting a parameter and setting a different
- * one, so we unset recoveryTarget when the parameter is set to an empty
- * string.
- *
- * XXX this code is broken by design. Throwing an error from a GUC assign
- * hook breaks fundamental assumptions of guc.c. So long as all the variables
- * for which this can happen are PGC_POSTMASTER, the consequences are limited,
- * since we'd just abort postmaster startup anyway. Nonetheless it's likely
- * that we have odd behaviors such as unexpected GUC ordering dependencies.
+ * Return the recovery target derived from the recovery_target* settings,
+ * raising an error if more than one of them is set.
*/
-
-pg_noreturn static void
-error_multiple_recovery_targets(void)
+static RecoveryTargetType
+DetermineRecoveryTargetType(void)
{
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("multiple recovery targets specified"),
- errdetail("At most one of \"recovery_target\", \"recovery_target_lsn\", \"recovery_target_name\", \"recovery_target_time\", \"recovery_target_xid\" may be set.")));
+ int ntargets = 0;
+ RecoveryTargetType target = RECOVERY_TARGET_UNSET;
+ const char *val;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+
+#define ADD_TARGET_IF_SET(gucname, kind) \
+ do { \
+ val = GetConfigOption(gucname, false, false); \
+ if (val[0] != '\0') \
+ { \
+ ntargets++; \
+ target = (kind); \
+ if (buf.len == 0) \
+ appendStringInfo(&buf, _("\"%s\""), gucname); \
+ else \
+ appendStringInfo(&buf, _(", \"%s\""), gucname); \
+ } \
+ } while (0)
+
+ ADD_TARGET_IF_SET("recovery_target", RECOVERY_TARGET_IMMEDIATE);
+ ADD_TARGET_IF_SET("recovery_target_lsn", RECOVERY_TARGET_LSN);
+ ADD_TARGET_IF_SET("recovery_target_name", RECOVERY_TARGET_NAME);
+ ADD_TARGET_IF_SET("recovery_target_time", RECOVERY_TARGET_TIME);
+ ADD_TARGET_IF_SET("recovery_target_xid", RECOVERY_TARGET_XID);
+#undef ADD_TARGET_IF_SET
+
+ if (ntargets > 1)
+ ereport(FATAL,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot specify more than one recovery target"),
+ errdetail("Parameters set are: %s.",
+ buf.data));
+
+ pfree(buf.data);
+
+ return target;
}
/*
return true;
}
-/*
- * GUC assign_hook for recovery_target
- */
-void
-assign_recovery_target(const char *newval, void *extra)
-{
- if (recoveryTarget != RECOVERY_TARGET_UNSET &&
- recoveryTarget != RECOVERY_TARGET_IMMEDIATE)
- error_multiple_recovery_targets();
-
- if (newval && strcmp(newval, "") != 0)
- recoveryTarget = RECOVERY_TARGET_IMMEDIATE;
- else
- recoveryTarget = RECOVERY_TARGET_UNSET;
-}
-
/*
* GUC check_hook for recovery_target_lsn
*/
void
assign_recovery_target_lsn(const char *newval, void *extra)
{
- if (recoveryTarget != RECOVERY_TARGET_UNSET &&
- recoveryTarget != RECOVERY_TARGET_LSN)
- error_multiple_recovery_targets();
-
if (newval && strcmp(newval, "") != 0)
- {
- recoveryTarget = RECOVERY_TARGET_LSN;
recoveryTargetLSN = *((XLogRecPtr *) extra);
- }
- else
- recoveryTarget = RECOVERY_TARGET_UNSET;
}
/*
return true;
}
-/*
- * GUC assign_hook for recovery_target_name
- */
-void
-assign_recovery_target_name(const char *newval, void *extra)
-{
- if (recoveryTarget != RECOVERY_TARGET_UNSET &&
- recoveryTarget != RECOVERY_TARGET_NAME)
- error_multiple_recovery_targets();
-
- if (newval && strcmp(newval, "") != 0)
- {
- recoveryTarget = RECOVERY_TARGET_NAME;
- recoveryTargetName = newval;
- }
- else
- recoveryTarget = RECOVERY_TARGET_UNSET;
-}
-
/*
* GUC check_hook for recovery_target_time
*
return true;
}
-/*
- * GUC assign_hook for recovery_target_time
- */
-void
-assign_recovery_target_time(const char *newval, void *extra)
-{
- if (recoveryTarget != RECOVERY_TARGET_UNSET &&
- recoveryTarget != RECOVERY_TARGET_TIME)
- error_multiple_recovery_targets();
-
- if (newval && strcmp(newval, "") != 0)
- recoveryTarget = RECOVERY_TARGET_TIME;
- else
- recoveryTarget = RECOVERY_TARGET_UNSET;
-}
-
/*
* GUC check_hook for recovery_target_timeline
*/
void
assign_recovery_target_xid(const char *newval, void *extra)
{
- if (recoveryTarget != RECOVERY_TARGET_UNSET &&
- recoveryTarget != RECOVERY_TARGET_XID)
- error_multiple_recovery_targets();
-
if (newval && strcmp(newval, "") != 0)
- {
- recoveryTarget = RECOVERY_TARGET_XID;
recoveryTargetXid = *((TransactionId *) extra);
- }
- else
- recoveryTarget = RECOVERY_TARGET_UNSET;
}
variable => 'recovery_target_string',
boot_val => '""',
check_hook => 'check_recovery_target',
- assign_hook => 'assign_recovery_target',
},
{ name => 'recovery_target_action', type => 'enum', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET',
{ name => 'recovery_target_name', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET',
short_desc => 'Sets the named restore point up to which recovery will proceed.',
- variable => 'recovery_target_name_string',
+ variable => 'recoveryTargetName',
boot_val => '""',
check_hook => 'check_recovery_target_name',
- assign_hook => 'assign_recovery_target_name',
},
{ name => 'recovery_target_time', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET',
variable => 'recovery_target_time_string',
boot_val => '""',
check_hook => 'check_recovery_target_time',
- assign_hook => 'assign_recovery_target_time',
},
{ name => 'recovery_target_timeline', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET',
static char *recovery_target_timeline_string;
static char *recovery_target_string;
static char *recovery_target_xid_string;
-static char *recovery_target_name_string;
static char *recovery_target_lsn_string;
/* should be static, but commands/variable.c needs to get at this */
extern PGDLLIMPORT TransactionId recoveryTargetXid;
extern PGDLLIMPORT char *recovery_target_time_string;
extern PGDLLIMPORT TimestampTz recoveryTargetTime;
-extern PGDLLIMPORT const char *recoveryTargetName;
+extern PGDLLIMPORT char *recoveryTargetName;
extern PGDLLIMPORT XLogRecPtr recoveryTargetLSN;
extern PGDLLIMPORT RecoveryTargetType recoveryTarget;
extern PGDLLIMPORT bool wal_receiver_create_temp_slot;
extern void assign_recovery_prefetch(int new_value, void *extra);
extern bool check_recovery_target(char **newval, void **extra,
GucSource source);
-extern void assign_recovery_target(const char *newval, void *extra);
extern bool check_recovery_target_lsn(char **newval, void **extra,
GucSource source);
extern void assign_recovery_target_lsn(const char *newval, void *extra);
extern bool check_recovery_target_name(char **newval, void **extra,
GucSource source);
-extern void assign_recovery_target_name(const char *newval, void *extra);
extern bool check_recovery_target_time(char **newval, void **extra,
GucSource source);
-extern void assign_recovery_target_time(const char *newval, void *extra);
extern bool check_recovery_target_timeline(char **newval, void **extra,
GucSource source);
extern void assign_recovery_target_timeline(const char *newval, void *extra);
# Create and test a standby from given backup, with a certain recovery target.
# Choose $until_lsn later than the transaction commit that causes the row
# count to reach $num_rows, yet not later than the recovery target.
+# If options is given, pass it through Cluster->start(options => ...). This is
+# used to exercise scenarios that require the postmaster command line to receive
+# multiple "-c name=value" instances of the same GUC, which postgresql.conf
+# cannot express because ProcessConfigFile collapses duplicate keys.
sub test_recovery_standby
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $recovery_params = shift;
my $num_rows = shift;
my $until_lsn = shift;
+ my %params = @_;
my $node_standby = PostgreSQL::Test::Cluster->new($node_name);
$node_standby->init_from_backup($node_primary, 'my_backup',
$node_standby->append_conf('postgresql.conf', qq($param_item));
}
- $node_standby->start;
+ $node_standby->start(
+ defined $params{options} ? (options => $params{options}) : ());
# Wait until standby has replayed enough data
my $caughtup_query =
# Force archiving of WAL file
$node_primary->safe_psql('postgres', "SELECT pg_switch_wal()");
+my $lsn6 =
+ $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+
# Test recovery targets
my @recovery_params = ("recovery_target = 'immediate'");
test_recovery_standby('immediate target',
test_recovery_standby('LSN', 'standby_5', $node_primary, \@recovery_params,
"5000", $lsn5);
+# Regression: empty-string for one recovery_target_* GUC must not clobber
+# another non-empty target. Setting recovery_target_xid + recovery_target_time
+# = '' must recover to the xid, not run as no-target recovery.
+@recovery_params =
+ ("recovery_target_xid = '$recovery_txid'", "recovery_target_time = ''");
+test_recovery_standby('xid with empty time GUC',
+ 'standby_xid_empty_time', $node_primary, \@recovery_params,
+ "2000", $lsn2);
+
# Multiple targets
#
-# Multiple conflicting settings are not allowed, but setting the same
-# parameter multiple times or unsetting a parameter and setting a
-# different one is allowed.
+# Multiple conflicting non-empty settings are rejected, but setting the same
+# parameter twice or clearing one with an empty string is allowed.
@recovery_params = (
"recovery_target_name = '$recovery_name'",
test_recovery_standby('multiple overriding settings',
'standby_6', $node_primary, \@recovery_params, "3000", $lsn3);
-my $node_standby = PostgreSQL::Test::Cluster->new('standby_7');
-$node_standby->init_from_backup($node_primary, 'my_backup',
- has_restoring => 1);
-$node_standby->append_conf(
- 'postgresql.conf', "recovery_target_name = '$recovery_name'
-recovery_target_time = '$recovery_time'");
-
-my $res = run_log(
- [
- 'pg_ctl',
- '--pgdata' => $node_standby->data_dir,
- '--log' => $node_standby->logfile,
- 'start',
- ]);
-ok(!$res, 'invalid recovery startup fails');
-
-my $logfile = slurp_file($node_standby->logfile());
-like(
- $logfile,
- qr/multiple recovery targets specified/,
- 'multiple conflicting settings');
-
# Check behavior when recovery ends before target is reached
-$node_standby = PostgreSQL::Test::Cluster->new('standby_8');
+my $node_standby = PostgreSQL::Test::Cluster->new('standby_8');
$node_standby->init_from_backup(
$node_primary, 'my_backup',
has_restoring => 1,
last if !-f $node_standby->data_dir . '/postmaster.pid';
usleep(100_000);
}
-$logfile = slurp_file($node_standby->logfile());
+my $logfile = slurp_file($node_standby->logfile());
like(
$logfile,
qr/FATAL: .* recovery ended before configured recovery target was reached/,
'recovery end before target reached is a fatal error');
+# Conflicts are rejected at every startup, even without recovery.signal.
+# init_from_backup without has_restoring creates no recovery.signal, so this
+# cluster would otherwise start as a plain primary; the conflict must still be
+# caught.
+my $node_no_signal = PostgreSQL::Test::Cluster->new('multi_target_no_signal');
+$node_no_signal->init_from_backup($node_primary, 'my_backup');
+$node_no_signal->append_conf(
+ 'postgresql.conf', "recovery_target_name = '$recovery_name'
+recovery_target_time = '$recovery_time'");
+
+ok( !$node_no_signal->start(fail_ok => 1),
+ 'server fails to start with conflicting recovery targets and no recovery.signal'
+);
+
+my $logfile_no_signal = slurp_file($node_no_signal->logfile());
+like(
+ $logfile_no_signal,
+ qr/cannot specify more than one recovery target/,
+ 'expected error message logged without recovery.signal');
+like(
+ $logfile_no_signal,
+ qr/Parameters set are: "recovery_target_name", "recovery_target_time"/,
+ 'errdetail lists the set parameters in order without recovery.signal');
+unlike(
+ $logfile_no_signal,
+ qr/Parameters set are:[^\n]*=/,
+ 'errdetail does not echo parameter values without recovery.signal');
+
+my $node_immediate_conflict =
+ PostgreSQL::Test::Cluster->new('immediate_target_conflict');
+$node_immediate_conflict->init_from_backup($node_primary, 'my_backup');
+$node_immediate_conflict->append_conf(
+ 'postgresql.conf',
+ "recovery_target = 'immediate'
+recovery_target_xid = '$recovery_txid'");
+
+ok( !$node_immediate_conflict->start(fail_ok => 1),
+ 'server fails to start with recovery_target=immediate and a second target'
+);
+like(
+ slurp_file($node_immediate_conflict->logfile()),
+ qr/cannot specify more than one recovery target/,
+ 'recovery_target=immediate conflicting with another target is rejected');
+
+# Same-GUC set-then-clear: setting a recovery_target_* GUC and then setting the
+# same GUC to an empty string leaves no target, so recovery runs to the end of
+# WAL. Duplicate keys collapse in postgresql.conf, so "pg_ctl --options" passes
+# both assignments on the postmaster command line.
+test_recovery_standby(
+ 'recovery_target_xid set then cleared',
+ 'standby_xid_set_clear',
+ $node_primary,
+ [],
+ "6000",
+ $lsn6,
+ options => "-c recovery_target_xid=$recovery_txid -c recovery_target_xid="
+);
+
+# Set recovery_target_xid, then set and clear recovery_target_name. Only the
+# xid remains, so recovery must stop at it rather than running to the end of WAL
+# (a competing target that is set then cleared must not strand the first one).
+test_recovery_standby(
+ 'recovery target preserved when a competing one is set then cleared',
+ 'standby_clobber_clear',
+ $node_primary,
+ [],
+ "2000",
+ $lsn2,
+ options =>
+ "-c recovery_target_xid=$recovery_txid -c recovery_target_name=$recovery_name -c recovery_target_name="
+);
+
# Invalid recovery_target_timeline tests
my ($result, $stdout, $stderr) = $node_primary->psql('postgres',
"ALTER SYSTEM SET recovery_target_timeline TO 'bogus'");