From 1a4ab9ec23f0635a4c15b069df60b545814650e9 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 7 Mar 2011 21:02:40 +0200 Subject: [PATCH] If recovery_target_timeline is set to 'latest' and standby mode is enabled, periodically rescan the archive for new timelines, while waiting for new WAL segments to arrive. This allows you to set up a standby server that follows the TLI change if another standby server is promoted to master. Before this, you had to restart the standby server to make it notice the new timeline. This patch only scans the archive for TLI changes, it won't follow a TLI change in streaming replication. That is much needed too, but it would be a much bigger patch than I dare to sneak in this late in the release cycle. There was discussion on improving the sanity checking of the WAL segments so that the system would notice more reliably if the new timeline isn't an ancestor of the current one, but that is not included in this patch. Reviewed by Fujii Masao. --- doc/src/sgml/high-availability.sgml | 5 +- doc/src/sgml/recovery-config.sgml | 4 +- src/backend/access/transam/xlog.c | 79 +++++++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml index e33d315137..b97167fc50 100644 --- a/doc/src/sgml/high-availability.sgml +++ b/doc/src/sgml/high-availability.sgml @@ -660,7 +660,10 @@ protocol to make nodes agree on a serializable transactional order. command file recovery.conf in the standby's cluster data directory, and turn on standby_mode. Set restore_command to a simple command to copy files from - the WAL archive. + the WAL archive. If you plan to have multiple standby servers for high + availability purposes, set recovery_target_timeline to + latest, to make the standby server follow the timeline change + that occurs at failover to another standby. diff --git a/doc/src/sgml/recovery-config.sgml b/doc/src/sgml/recovery-config.sgml index 602fbe2c76..e9e95acb8d 100644 --- a/doc/src/sgml/recovery-config.sgml +++ b/doc/src/sgml/recovery-config.sgml @@ -240,7 +240,9 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows Specifies recovering into a particular timeline. The default is to recover along the same timeline that was current when the - base backup was taken. You only need to set this parameter + base backup was taken. Setting this to latest recovers + to the latest timeline found in the archive, which is useful in + a standby server. Other than that you only need to set this parameter in complex re-recovery situations, where you need to return to a state that itself was reached after a point-in-time recovery. See for discussion. diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index b4eb4ac2cc..f86d9ebdda 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -214,6 +214,8 @@ static bool recoveryStopAfter; * * recoveryTargetTLI: the desired timeline that we want to end in. * + * recoveryTargetIsLatest: was the requested target timeline 'latest'? + * * expectedTLIs: an integer list of recoveryTargetTLI and the TLIs of * its known parents, newest first (so recoveryTargetTLI is always the * first list member). Only these TLIs are expected to be seen in the WAL @@ -227,6 +229,7 @@ static bool recoveryStopAfter; * to decrease. */ static TimeLineID recoveryTargetTLI; +static bool recoveryTargetIsLatest = false; static List *expectedTLIs; static TimeLineID curFileTLI; @@ -637,6 +640,7 @@ static bool ValidXLOGHeader(XLogPageHeader hdr, int emode); static XLogRecord *ReadCheckpointRecord(XLogRecPtr RecPtr, int whichChkpt); static List *readTimeLineHistory(TimeLineID targetTLI); static bool existsTimeLineHistory(TimeLineID probeTLI); +static bool rescanLatestTimeLine(void); static TimeLineID findNewestTimeLine(TimeLineID startTLI); static void writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, TimeLineID endTLI, @@ -4253,6 +4257,62 @@ existsTimeLineHistory(TimeLineID probeTLI) } } +/* + * Scan for new timelines that might have appeared in the archive since we + * started recovery. + * + * If there are any, the function changes recovery target TLI to the latest + * one and returns 'true'. + */ +static bool +rescanLatestTimeLine(void) +{ + TimeLineID newtarget; + newtarget = findNewestTimeLine(recoveryTargetTLI); + if (newtarget != recoveryTargetTLI) + { + /* + * Determine the list of expected TLIs for the new TLI + */ + List *newExpectedTLIs; + newExpectedTLIs = readTimeLineHistory(newtarget); + + /* + * If the current timeline is not part of the history of the + * new timeline, we cannot proceed to it. + * + * XXX This isn't foolproof: The new timeline might have forked from + * the current one, but before the current recovery location. In that + * case we will still switch to the new timeline and proceed replaying + * from it even though the history doesn't match what we already + * replayed. That's not good. We will likely notice at the next online + * checkpoint, as the TLI won't match what we expected, but it's + * not guaranteed. The admin needs to make sure that doesn't happen. + */ + if (!list_member_int(newExpectedTLIs, + (int) recoveryTargetTLI)) + ereport(LOG, + (errmsg("new timeline %u is not a child of database system timeline %u", + newtarget, + ThisTimeLineID))); + else + { + /* Switch target */ + recoveryTargetTLI = newtarget; + list_free(expectedTLIs); + expectedTLIs = newExpectedTLIs; + + XLogCtl->RecoveryTargetTLI = recoveryTargetTLI; + + ereport(LOG, + (errmsg("new target timeline is %u", + recoveryTargetTLI))); + return true; + } + } + return false; +} + /* * Find the newest existing timeline, assuming that startTLI exists. * @@ -5327,11 +5387,13 @@ readRecoveryCommandFile(void) (errmsg("recovery target timeline %u does not exist", rtli))); recoveryTargetTLI = rtli; + recoveryTargetIsLatest = false; } else { /* We start the "latest" search from pg_control's timeline */ recoveryTargetTLI = findNewestTimeLine(recoveryTargetTLI); + recoveryTargetIsLatest = true; } } @@ -10032,13 +10094,24 @@ retry: { /* * We've exhausted all options for retrieving the - * file. Retry ... + * file. Retry. */ failedSources = 0; /* - * ... but sleep first if it hasn't been long since - * last attempt. + * Before we sleep, re-scan for possible new timelines + * if we were requested to recover to the latest + * timeline. + */ + if (recoveryTargetIsLatest) + { + if (rescanLatestTimeLine()) + continue; + } + + /* + * If it hasn't been long since last attempt, sleep + * to avoid busy-waiting. */ now = (pg_time_t) time(NULL); if ((now - last_fail_time) < 5)