Be clear about whether a recovery pause has taken effect.
authorRobert Haas <rhaas@postgresql.org>
Thu, 11 Mar 2021 19:52:32 +0000 (14:52 -0500)
committerRobert Haas <rhaas@postgresql.org>
Thu, 11 Mar 2021 20:07:03 +0000 (15:07 -0500)
Previously, the code and documentation seem to have essentially
assumed than a call to pg_wal_replay_pause() would take place
immediately, but that's not the case, because we only check for a
pause in certain places. This means that a tool that uses this
function and then wants to do something else afterward that is
dependent on the pause having taken effect doesn't know how long it
needs to wait to be sure that no more WAL is going to be replayed.

To avoid that, add a new function pg_get_wal_replay_pause_state()
which returns either 'not paused', 'paused requested', or 'paused'.
After calling pg_wal_replay_pause() the status will immediate change
from 'not paused' to 'pause requested'; when the startup process
has noticed this, the status will change to 'pause'.  For backward
compatibility, pg_is_wal_replay_paused() still exists and returns
the same thing as before: true if a pause has been requested,
whether or not it has taken effect yet; and false if not.
The documentation is updated to clarify.

To improve the changes that a pause request is quickly confirmed
effective, adjust things so that WaitForWALToBecomeAvailable will
swiftly reach a call to recoveryPausesHere() when a pause request
is made.

Dilip Kumar, reviewed by Simon Riggs, Kyotaro Horiguchi, Yugo Nagata,
Masahiko Sawada, and Bharath Rupireddy.

Discussion: http://postgr.es/m/CAFiTN-vcLLWEm8Zr%3DYK83rgYrT9pbC8VJCfa1kY9vL3AUPfu6g%40mail.gmail.com

doc/src/sgml/func.sgml
src/backend/access/transam/xlog.c
src/backend/access/transam/xlogfuncs.c
src/include/access/xlog.h
src/include/catalog/pg_proc.dat

index ece09699ef8f92c8770e5738aae70aad76e53f73..9492a3c6b924402ae6cf91b07b115dd89eb4bf8a 100644 (file)
@@ -25344,7 +25344,24 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
-        Returns true if recovery is paused.
+        Returns true if recovery pause is requested.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_wal_replay_pause_state</primary>
+        </indexterm>
+        <function>pg_get_wal_replay_pause_state</function> ()
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Returns recovery pause state.  The return values are <literal>
+        not paused</literal> if pause is not requested, <literal>
+        pause requested</literal> if pause is requested but recovery is
+        not yet paused and, <literal>paused</literal> if the recovery is
+        actually paused.
        </para></entry>
       </row>
 
@@ -25383,10 +25400,15 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
         <returnvalue>void</returnvalue>
        </para>
        <para>
-        Pauses recovery.  While recovery is paused, no further database
-        changes are applied.  If hot standby is active, all new queries will
-        see the same consistent snapshot of the database, and no further query
-        conflicts will be generated until recovery is resumed.
+        Request to pause recovery.  A request doesn't mean that recovery stops
+        right away.  If you want a guarantee that recovery is actually paused,
+        you need to check for the recovery pause state returned by
+        <function>pg_get_wal_replay_pause_state()</function>.  Note that
+        <function>pg_is_wal_replay_paused()</function> returns whether a request
+        is made.  While recovery is paused, no further database changes are applied.
+        If hot standby is active, all new queries will see the same consistent
+        snapshot of the database, and no further query conflicts will be generated
+        until recovery is resumed.
        </para>
        <para>
         This function is restricted to superusers by default, but other users
index 18af3d41209de70751f774af87571c8cc895c509..e04250f4e9e702950958204d6bcb53a2ea9f20c1 100644 (file)
@@ -722,8 +722,8 @@ typedef struct XLogCtlData
     * only relevant for replication or archive recovery
     */
    TimestampTz currentChunkStartTime;
-   /* Are we requested to pause recovery? */
-   bool        recoveryPause;
+   /* Recovery pause state */
+   RecoveryPauseState  recoveryPauseState;
 
    /*
     * lastFpwDisableRecPtr points to the start of the last replayed
@@ -895,6 +895,7 @@ static void validateRecoveryParameters(void);
 static void exitArchiveRecovery(TimeLineID endTLI, XLogRecPtr endOfLog);
 static bool recoveryStopsBefore(XLogReaderState *record);
 static bool recoveryStopsAfter(XLogReaderState *record);
+static void ConfirmRecoveryPaused(void);
 static void recoveryPausesHere(bool endOfRecovery);
 static bool recoveryApplyDelay(XLogReaderState *record);
 static void SetLatestXTime(TimestampTz xtime);
@@ -6034,7 +6035,7 @@ recoveryStopsAfter(XLogReaderState *record)
 }
 
 /*
- * Wait until shared recoveryPause flag is cleared.
+ * Wait until shared recoveryPauseState is set to RECOVERY_NOT_PAUSED.
  *
  * endOfRecovery is true if the recovery target is reached and
  * the paused state starts at the end of recovery because of
@@ -6064,34 +6065,72 @@ recoveryPausesHere(bool endOfRecovery)
                (errmsg("recovery has paused"),
                 errhint("Execute pg_wal_replay_resume() to continue.")));
 
-   while (RecoveryIsPaused())
+   /* loop until recoveryPauseState is set to RECOVERY_NOT_PAUSED */
+   while (GetRecoveryPauseState() != RECOVERY_NOT_PAUSED)
    {
        HandleStartupProcInterrupts();
        if (CheckForStandbyTrigger())
            return;
        pgstat_report_wait_start(WAIT_EVENT_RECOVERY_PAUSE);
+
+       /*
+        * If recovery pause is requested then set it paused.  While we are in
+        * the loop, user might resume and pause again so set this every time.
+        */
+       ConfirmRecoveryPaused();
+
        pg_usleep(1000000L);    /* 1000 ms */
        pgstat_report_wait_end();
    }
 }
 
-bool
-RecoveryIsPaused(void)
+/*
+ * Get the current state of the recovery pause request.
+ */
+RecoveryPauseState
+GetRecoveryPauseState(void)
 {
-   bool        recoveryPause;
+   RecoveryPauseState  state;
 
    SpinLockAcquire(&XLogCtl->info_lck);
-   recoveryPause = XLogCtl->recoveryPause;
+   state = XLogCtl->recoveryPauseState;
    SpinLockRelease(&XLogCtl->info_lck);
 
-   return recoveryPause;
+   return state;
 }
 
+/*
+ * Set the recovery pause state.
+ *
+ * If recovery pause is requested then sets the recovery pause state to
+ * 'pause requested' if it is not already 'paused'.  Otherwise, sets it
+ * to 'not paused' to resume the recovery.  The recovery pause will be
+ * confirmed by the ConfirmRecoveryPaused.
+ */
 void
 SetRecoveryPause(bool recoveryPause)
 {
    SpinLockAcquire(&XLogCtl->info_lck);
-   XLogCtl->recoveryPause = recoveryPause;
+
+   if (!recoveryPause)
+       XLogCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
+   else if (XLogCtl->recoveryPauseState == RECOVERY_NOT_PAUSED)
+       XLogCtl->recoveryPauseState = RECOVERY_PAUSE_REQUESTED;
+
+   SpinLockRelease(&XLogCtl->info_lck);
+}
+
+/*
+ * Confirm the recovery pause by setting the recovery pause state to
+ * RECOVERY_PAUSED.
+ */
+static void
+ConfirmRecoveryPaused(void)
+{
+   /* If recovery pause is requested then set it paused */
+   SpinLockAcquire(&XLogCtl->info_lck);
+   if (XLogCtl->recoveryPauseState == RECOVERY_PAUSE_REQUESTED)
+       XLogCtl->recoveryPauseState = RECOVERY_PAUSED;
    SpinLockRelease(&XLogCtl->info_lck);
 }
 
@@ -6292,7 +6331,7 @@ RecoveryRequiresIntParameter(const char *param_name, int currValue, int minValue
                     errdetail("If recovery is unpaused, the server will shut down."),
                     errhint("You can then restart the server after making the necessary configuration changes.")));
 
-           while (RecoveryIsPaused())
+           while (GetRecoveryPauseState() != RECOVERY_NOT_PAUSED)
            {
                HandleStartupProcInterrupts();
 
@@ -6311,6 +6350,13 @@ RecoveryRequiresIntParameter(const char *param_name, int currValue, int minValue
                    warned_for_promote = true;
                }
 
+               /*
+                * If recovery pause is requested then set it paused.  While we
+                * are in the loop, user might resume and pause again so set
+                * this every time.
+                */
+               ConfirmRecoveryPaused();
+
                pgstat_report_wait_start(WAIT_EVENT_RECOVERY_PAUSE);
                pg_usleep(1000000L);    /* 1000 ms */
                pgstat_report_wait_end();
@@ -7205,7 +7251,7 @@ StartupXLOG(void)
        XLogCtl->lastReplayedTLI = XLogCtl->replayEndTLI;
        XLogCtl->recoveryLastXTime = 0;
        XLogCtl->currentChunkStartTime = 0;
-       XLogCtl->recoveryPause = false;
+       XLogCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
        SpinLockRelease(&XLogCtl->info_lck);
 
        /* Also ensure XLogReceiptTime has a sane value */
@@ -7309,7 +7355,8 @@ StartupXLOG(void)
                 * otherwise would is a minor issue, so it doesn't seem worth
                 * adding another spinlock cycle to prevent that.
                 */
-               if (((volatile XLogCtlData *) XLogCtl)->recoveryPause)
+               if (((volatile XLogCtlData *) XLogCtl)->recoveryPauseState !=
+                   RECOVERY_NOT_PAUSED)
                    recoveryPausesHere(false);
 
                /*
@@ -7334,7 +7381,8 @@ StartupXLOG(void)
                     * here otherwise pausing during the delay-wait wouldn't
                     * work.
                     */
-                   if (((volatile XLogCtlData *) XLogCtl)->recoveryPause)
+                   if (((volatile XLogCtlData *) XLogCtl)->recoveryPauseState !=
+                       RECOVERY_NOT_PAUSED)
                        recoveryPausesHere(false);
                }
 
@@ -12656,6 +12704,14 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess,
                elog(ERROR, "unexpected WAL source %d", currentSource);
        }
 
+       /*
+        * Check for recovery pause here so that we can confirm more quickly
+        * that a requested pause has actually taken effect.
+        */
+       if (((volatile XLogCtlData *) XLogCtl)->recoveryPauseState !=
+           RECOVERY_NOT_PAUSED)
+           recoveryPausesHere(false);
+
        /*
         * This possibly-long loop needs to handle interrupts of startup
         * process.
index d8c5bf6dc2958d7a34717a6a9a15d477cf934d2c..daa4a113b7df1800a083ebd4a199db0c2b362d6b 100644 (file)
@@ -517,7 +517,7 @@ pg_walfile_name(PG_FUNCTION_ARGS)
 }
 
 /*
- * pg_wal_replay_pause - pause recovery now
+ * pg_wal_replay_pause - Request to pause recovery
  *
  * Permission checking for this function is managed through the normal
  * GRANT system.
@@ -540,6 +540,9 @@ pg_wal_replay_pause(PG_FUNCTION_ARGS)
 
    SetRecoveryPause(true);
 
+   /* wake up the recovery process so that it can process the pause request */
+   WakeupRecovery();
+
    PG_RETURN_VOID();
 }
 
@@ -582,7 +585,45 @@ pg_is_wal_replay_paused(PG_FUNCTION_ARGS)
                 errmsg("recovery is not in progress"),
                 errhint("Recovery control functions can only be executed during recovery.")));
 
-   PG_RETURN_BOOL(RecoveryIsPaused());
+   PG_RETURN_BOOL(GetRecoveryPauseState() != RECOVERY_NOT_PAUSED);
+}
+
+/*
+ * pg_get_wal_replay_pause_state - Returns the recovery pause state.
+ *
+ * Returned values:
+ *
+ * 'not paused' - if pause is not requested
+ * 'pause requested' - if pause is requested but recovery is not yet paused
+ * 'paused' - if recovery is paused
+ */
+Datum
+pg_get_wal_replay_pause_state(PG_FUNCTION_ARGS)
+{
+   char    *statestr = NULL;
+
+   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.")));
+
+   /* get the recovery pause state */
+   switch(GetRecoveryPauseState())
+   {
+       case RECOVERY_NOT_PAUSED:
+           statestr = "not paused";
+           break;
+       case RECOVERY_PAUSE_REQUESTED:
+           statestr = "pause requested";
+           break;
+       case RECOVERY_PAUSED:
+           statestr = "paused";
+           break;
+   }
+
+   Assert(statestr != NULL);
+   PG_RETURN_TEXT_P(cstring_to_text(statestr));
 }
 
 /*
index 1e53d9d4ca66cb3c1f17a3b271b168af76fd9126..6d384d3ce6dace1d1408f6e98379a7e060e38f39 100644 (file)
@@ -175,6 +175,14 @@ typedef enum RecoveryState
    RECOVERY_STATE_DONE         /* currently in production */
 } RecoveryState;
 
+/* Recovery pause states */
+typedef enum RecoveryPauseState
+{
+   RECOVERY_NOT_PAUSED,        /* pause not requested */
+   RECOVERY_PAUSE_REQUESTED,   /* pause requested, but not yet paused */
+   RECOVERY_PAUSED             /* recovery is paused */
+} RecoveryPauseState;
+
 extern PGDLLIMPORT int wal_level;
 
 /* Is WAL archiving enabled (always or only while server is running normally)? */
@@ -311,7 +319,7 @@ extern void GetXLogReceiptTime(TimestampTz *rtime, bool *fromStream);
 extern XLogRecPtr GetXLogReplayRecPtr(TimeLineID *replayTLI);
 extern XLogRecPtr GetXLogInsertRecPtr(void);
 extern XLogRecPtr GetXLogWriteRecPtr(void);
-extern bool RecoveryIsPaused(void);
+extern RecoveryPauseState GetRecoveryPauseState(void);
 extern void SetRecoveryPause(bool recoveryPause);
 extern TimestampTz GetLatestXTime(void);
 extern TimestampTz GetCurrentChunkReplayStartTime(void);
index c7619f8cd30c274913b8b082d6f74f293e7fa1a1..61361a6bc932847cf070535291a02a63b7f68416 100644 (file)
   proname => 'pg_is_wal_replay_paused', provolatile => 'v',
   prorettype => 'bool', proargtypes => '',
   prosrc => 'pg_is_wal_replay_paused' },
+{ oid => '1137', descr => 'get wal replay pause state',
+  proname => 'pg_get_wal_replay_pause_state', provolatile => 'v',
+  prorettype => 'text', proargtypes => '',
+  prosrc => 'pg_get_wal_replay_pause_state' },
 
 { oid => '2621', descr => 'reload configuration files',
   proname => 'pg_reload_conf', provolatile => 'v', prorettype => 'bool',