agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
43+ messages / 3 participants
[nested] [flat]

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v3 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 363 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 ++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 528 insertions(+), 454 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b..9927a45a07 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
 
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
 
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+rewind_source *
+init_local_source(const char *datadir)
 {
-	recurse_dir(datadir, NULL, callback);
+	local_source *src;
+
+	src = pg_malloc0(sizeof(local_source));
+
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
+
+	src->datadir = datadir;
+
+	return &src->common;
 }
 
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
 static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
-
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
-
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
-
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
-
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
-
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295e..c8ee38f8e0 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b153..8be1a9582d 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0..4ae343888e 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf..c763085976 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d..52c4e147e1 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
+
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
+{
+	libpq_source *src;
+
+	init_libpq_conn(conn);
+
+	src = pg_malloc0(sizeof(libpq_source));
+
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
 {
+	PGresult   *res;
 	char	   *str;
-	PGresult   *res;
-
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
-
-	if (showprogress)
-		pg_log_info("connected to server");
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 13b4eab52b..c5ee70272a 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38..0dc3dbd525 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.18.4


----Next_Part(Fri_Sep_18_16_41_50_2020_369)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="v2_5-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------D93EDEBFB124D563B723F4BD
Content-Type: text/x-patch; charset=UTF-8;
 name="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0005-Allow-pg_rewind-to-use-a-standby-server-as-the-sourc.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-08-19 12:34  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-08-19 12:34 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.
---
 src/bin/pg_rewind/copy_fetch.c  | 239 +++++----------------
 src/bin/pg_rewind/fetch.c       |  97 ++++++---
 src/bin/pg_rewind/fetch.h       |  76 +++++--
 src/bin/pg_rewind/file_ops.c    | 129 +++++++++++-
 src/bin/pg_rewind/file_ops.h    |   3 +
 src/bin/pg_rewind/libpq_fetch.c | 361 +++++++++++++++-----------------
 src/bin/pg_rewind/pg_rewind.c   |  70 +++++--
 src/bin/pg_rewind/pg_rewind.h   |   5 -
 8 files changed, 527 insertions(+), 453 deletions(-)

diff --git a/src/bin/pg_rewind/copy_fetch.c b/src/bin/pg_rewind/copy_fetch.c
index 61aed8018b6..9927a45a07a 100644
--- a/src/bin/pg_rewind/copy_fetch.c
+++ b/src/bin/pg_rewind/copy_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * copy_fetch.c
- *	  Functions for using a data directory as the source.
+ *	  Functions for using a local data directory as the source.
  *
  * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,8 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -20,146 +18,70 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-static void recurse_dir(const char *datadir, const char *path,
-						process_file_callback_t callback);
-
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-
-/*
- * Traverse through all files in a data directory, calling 'callback'
- * for each file.
- */
-void
-traverse_datadir(const char *datadir, process_file_callback_t callback)
+typedef struct
 {
-	recurse_dir(datadir, NULL, callback);
-}
-
-/*
- * recursive part of traverse_datadir
- *
- * parentpath is the current subdirectory's path relative to datadir,
- * or NULL at the top level.
- */
-static void
-recurse_dir(const char *datadir, const char *parentpath,
-			process_file_callback_t callback)
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   uint64 off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
 {
-	DIR		   *xldir;
-	struct dirent *xlde;
-	char		fullparentpath[MAXPGPATH];
+	local_source *src;
 
-	if (parentpath)
-		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
-	else
-		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+	src = pg_malloc0(sizeof(local_source));
 
-	xldir = opendir(fullparentpath);
-	if (xldir == NULL)
-		pg_fatal("could not open directory \"%s\": %m",
-				 fullparentpath);
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
 
-	while (errno = 0, (xlde = readdir(xldir)) != NULL)
-	{
-		struct stat fst;
-		char		fullpath[MAXPGPATH * 2];
-		char		path[MAXPGPATH * 2];
+	src->datadir = datadir;
 
-		if (strcmp(xlde->d_name, ".") == 0 ||
-			strcmp(xlde->d_name, "..") == 0)
-			continue;
-
-		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
-
-		if (lstat(fullpath, &fst) < 0)
-		{
-			if (errno == ENOENT)
-			{
-				/*
-				 * File doesn't exist anymore. This is ok, if the new primary
-				 * is running and the file was just removed. If it was a data
-				 * file, there should be a WAL record of the removal. If it
-				 * was something else, it couldn't have been anyway.
-				 *
-				 * TODO: But complain if we're processing the target dir!
-				 */
-			}
-			else
-				pg_fatal("could not stat file \"%s\": %m",
-						 fullpath);
-		}
-
-		if (parentpath)
-			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
-		else
-			snprintf(path, sizeof(path), "%s", xlde->d_name);
-
-		if (S_ISREG(fst.st_mode))
-			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
-		else if (S_ISDIR(fst.st_mode))
-		{
-			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
-			/* recurse to handle subdirectories */
-			recurse_dir(datadir, path, callback);
-		}
-#ifndef WIN32
-		else if (S_ISLNK(fst.st_mode))
-#else
-		else if (pgwin32_is_junction(fullpath))
-#endif
-		{
-#if defined(HAVE_READLINK) || defined(WIN32)
-			char		link_target[MAXPGPATH];
-			int			len;
-
-			len = readlink(fullpath, link_target, sizeof(link_target));
-			if (len < 0)
-				pg_fatal("could not read symbolic link \"%s\": %m",
-						 fullpath);
-			if (len >= sizeof(link_target))
-				pg_fatal("symbolic link \"%s\" target is too long",
-						 fullpath);
-			link_target[len] = '\0';
-
-			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
-
-			/*
-			 * If it's a symlink within pg_tblspc, we need to recurse into it,
-			 * to process all the tablespaces.  We also follow a symlink if
-			 * it's for pg_wal.  Symlinks elsewhere are ignored.
-			 */
-			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
-				strcmp(path, "pg_wal") == 0)
-				recurse_dir(datadir, path, callback);
-#else
-			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
-					 fullpath);
-#endif							/* HAVE_READLINK */
-		}
-	}
+	return &src->common;
+}
 
-	if (errno)
-		pg_fatal("could not read directory \"%s\": %m",
-				 fullparentpath);
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
 
-	if (closedir(xldir))
-		pg_fatal("could not close directory \"%s\": %m",
-				 fullparentpath);
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
 }
 
 /*
- * Copy a file from source to target, between 'begin' and 'end' offsets.
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
  *
  * If 'trunc' is true, any existing file with the same name is truncated.
  */
 static void
-rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
+local_fetch_file_range(rewind_source *source, const char *path, uint64 off,
+					   size_t len)
 {
+	const char *datadir = ((local_source *) source)->datadir;
 	PGAlignedBlock buf;
 	char		srcpath[MAXPGPATH];
 	int			srcfd;
+	uint64		begin = off;
+	uint64		end = off + len;
 
-	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir_source, path);
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
 
 	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
 	if (srcfd < 0)
@@ -169,7 +91,7 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 	if (lseek(srcfd, begin, SEEK_SET) == -1)
 		pg_fatal("could not seek in source file: %m");
 
-	open_target_file(path, trunc);
+	open_target_file(path, false);
 
 	while (end - begin > 0)
 	{
@@ -197,70 +119,17 @@ rewind_copy_file_range(const char *path, off_t begin, off_t end, bool trunc)
 		pg_fatal("could not close file \"%s\": %m", srcpath);
 }
 
-/*
- * Copy all relation data files from datadir_source to datadir_target, which
- * are marked in the given data page map.
- */
-void
-copy_executeFileMap(filemap_t *map)
+static void
+local_finish_fetch(rewind_source *source)
 {
-	file_entry_t *entry;
-	int			i;
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* ok, do nothing.. */
-				break;
-
-			case FILE_ACTION_COPY:
-				rewind_copy_file_range(entry->path, 0, entry->source_size, true);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				rewind_copy_file_range(entry->path, entry->target_size,
-									   entry->source_size, false);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	close_target_file();
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
 }
 
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+local_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-		rewind_copy_file_range(path, offset, offset + BLCKSZ, false);
-		/* Ok, this block has now been copied from new data dir to old */
-	}
-	pg_free(iter);
+	pfree(source);
 }
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
index f41d0f295ea..c8ee38f8e0b 100644
--- a/src/bin/pg_rewind/fetch.c
+++ b/src/bin/pg_rewind/fetch.c
@@ -24,37 +24,78 @@
 #include "filemap.h"
 #include "pg_rewind.h"
 
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
 /*
- * Fetch all relation data files that are marked in the given data page map.
+ * Execute the actions in the file map, fetching data from the source
+ * system as needed.
  */
 void
-execute_file_actions(filemap_t *filemap)
+execute_file_actions(filemap_t *filemap, rewind_source *source)
 {
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
+	int			i;
 
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
+	for (i = 0; i < filemap->nactions; i++)
+	{
+		file_entry_t *entry = filemap->actions[i];
+		datapagemap_iterator_t *iter;
+		BlockNumber blkno;
+		off_t		offset;
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		iter = datapagemap_iterate(&entry->target_modified_pages);
+		while (datapagemap_next(iter, &blkno))
+		{
+			offset = blkno * BLCKSZ;
+
+			source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+		}
+		pg_free(iter);
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges. XXX
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 }
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
index b20df8b1537..8be1a9582de 100644
--- a/src/bin/pg_rewind/fetch.h
+++ b/src/bin/pg_rewind/fetch.h
@@ -1,12 +1,12 @@
 /*-------------------------------------------------------------------------
  *
  * fetch.h
- *	  Fetching data from a local or remote data directory.
+ *	  Abstraction for fetching from source server.
  *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -16,29 +16,63 @@
 #define FETCH_H
 
 #include "access/xlogdefs.h"
-
+#include "file_ops.h"
 #include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, and write it
+	 * the corresponding file in the target system. The source implementation
+	 * may queue up the request and execute it later when convenient. Call
+	 * finish_fetch() to flush the queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   uint64 offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
 
 /*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
+ * Execute all the actions in 'filemap'.
  */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
 
 /* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
+extern rewind_source *init_libpq_source(PGconn *conn);
 
 /* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+extern rewind_source *init_local_source(const char *datadir);
 
 #endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..4ae343888ee 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_fetch.c
index 9c541bb73d5..52c4e147e10 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_fetch.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
  * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,11 +9,6 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
@@ -23,8 +18,6 @@
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
 
-PGconn	   *conn = NULL;
-
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
  *
@@ -34,30 +27,73 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									uint64 off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src = pg_malloc0(sizeof(libpq_source));
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
+
+	src->conn = conn;
+
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* we don't intend do any updates. Put the connection in read-only mode to keep us honest */
+	run_simple_command(conn, "SET default_transaction_read_only = off");
+
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +106,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +116,31 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
 
 	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
+	 * First create a temporary table, and COPY to load it with the list of
+	 * blocks that we need to fetch.
 	 */
-	run_simple_command("SET synchronous_commit = off");
+	run_simple_command(conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+	res = PQexec(conn, "COPY fetchchunks FROM STDIN");
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("could not send file list: %s",
+				 PQresultErrorMessage(res));
+	PQclear(res);
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +163,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +182,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +208,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,6 +289,48 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
+ */
+static void
+libpq_queue_fetch_range(rewind_source *source, const char *path, uint64 off,
+						size_t len)
+{
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		begin += len;
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+	}
+}
+
+
 /*----
  * Runs a query, which returns pieces of files from the remote source data
  * directory, and overwrites the corresponding parts of target files with
@@ -256,20 +341,46 @@ libpqProcessFileList(void)
  * chunk	bytea	-- file content
  *----
  */
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
 static void
-receiveFileChunks(const char *sql)
+libpq_finish_fetch(rewind_source *source)
 {
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -363,28 +474,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +506,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +514,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
+ * Close a libpq source.
  */
 static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
+libpq_destroy(rewind_source *source)
 {
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
- */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nactions; i++)
-	{
-		entry = map->actions[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_modified_pages, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
-static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
-{
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 2bdeed26c53..9e04a085226 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -35,8 +35,8 @@ static void usage(const char *progname);
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +69,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -269,19 +271,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +301,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +316,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -370,12 +389,12 @@ main(int argc, char **argv)
 				chkpttli);
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
 	filemap_init();
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -423,7 +442,7 @@ main(int argc, char **argv)
 	 * modified the target directory and there is no turning back!
 	 */
 
-	execute_file_actions(filemap);
+	execute_file_actions(filemap, source);
 
 	progress_report(true);
 
@@ -443,7 +462,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -465,6 +484,14 @@ main(int argc, char **argv)
 		WriteRecoveryConfig(conn, datadir_target,
 							GenerateRecoveryConfig(conn, NULL));
 
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
 	pg_log_info("Done!");
 
 	return 0;
@@ -627,7 +654,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -783,16 +810,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
-- 
2.20.1


--------------BF34D0120055DC3839060F92
Content-Type: text/x-patch; charset=UTF-8;
 name="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v2-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* [PATCH v3 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source.
@ 2020-09-24 16:51  Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2020-09-24 16:51 UTC (permalink / raw)

There copy_executeFileMap() and libpq_executeFileMap() contained basically
the same logic, just calling different functions to fetch the source files.
Refactor so that the common logic is in one place, execute_file_actions().

This makes the abstraction of a "source" server more clear, by introducing
a common abstract class, borrowing the object-oriented programming term,
that represents all the operations that can be done on the source server.
There are two implementations of it, one for fetching via libpq, and
another to fetch from a local directory. This adds some code, but makes it
easier to understand what's going on.

Reviewed-by: Kyotaro Horiguchi, Soumyadeep Chakraborty
Discussion: https://www.postgresql.org/message-id/0c5b3783-af52-3ee5-f8fa-6e794061f70d%40iki.fi
---
 src/bin/pg_rewind/Makefile                    |   5 +-
 src/bin/pg_rewind/fetch.c                     |  60 ---
 src/bin/pg_rewind/fetch.h                     |  44 --
 src/bin/pg_rewind/file_ops.c                  | 133 +++++-
 src/bin/pg_rewind/file_ops.h                  |   3 +
 .../{libpq_fetch.c => libpq_source.c}         | 395 +++++++++---------
 src/bin/pg_rewind/local_source.c              | 134 ++++++
 src/bin/pg_rewind/pg_rewind.c                 | 201 +++++++--
 src/bin/pg_rewind/pg_rewind.h                 |   5 -
 src/bin/pg_rewind/rewind_source.h             |  79 ++++
 10 files changed, 697 insertions(+), 362 deletions(-)
 delete mode 100644 src/bin/pg_rewind/fetch.c
 delete mode 100644 src/bin/pg_rewind/fetch.h
 rename src/bin/pg_rewind/{libpq_fetch.c => libpq_source.c} (65%)
 create mode 100644 src/bin/pg_rewind/local_source.c
 create mode 100644 src/bin/pg_rewind/rewind_source.h

diff --git a/src/bin/pg_rewind/Makefile b/src/bin/pg_rewind/Makefile
index f398c3d8488..9bfde5c087b 100644
--- a/src/bin/pg_rewind/Makefile
+++ b/src/bin/pg_rewind/Makefile
@@ -20,12 +20,11 @@ LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
 
 OBJS = \
 	$(WIN32RES) \
-	copy_fetch.o \
 	datapagemap.o \
-	fetch.o \
 	file_ops.o \
 	filemap.o \
-	libpq_fetch.o \
+	libpq_source.o \
+	local_source.o \
 	parsexlog.o \
 	pg_rewind.o \
 	timeline.o \
diff --git a/src/bin/pg_rewind/fetch.c b/src/bin/pg_rewind/fetch.c
deleted file mode 100644
index f41d0f295ea..00000000000
--- a/src/bin/pg_rewind/fetch.c
+++ /dev/null
@@ -1,60 +0,0 @@
-/*-------------------------------------------------------------------------
- *
- * fetch.c
- *	  Functions for fetching files from a local or remote data dir
- *
- * This file forms an abstraction of getting files from the "source".
- * There are two implementations of this interface: one for copying files
- * from a data directory via normal filesystem operations (copy_fetch.c),
- * and another for fetching files from a remote server via a libpq
- * connection (libpq_fetch.c)
- *
- *
- * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
- *
- *-------------------------------------------------------------------------
- */
-#include "postgres_fe.h"
-
-#include <sys/stat.h>
-#include <unistd.h>
-
-#include "fetch.h"
-#include "file_ops.h"
-#include "filemap.h"
-#include "pg_rewind.h"
-
-void
-fetchSourceFileList(void)
-{
-	if (datadir_source)
-		traverse_datadir(datadir_source, &process_source_file);
-	else
-		libpqProcessFileList();
-}
-
-/*
- * Fetch all relation data files that are marked in the given data page map.
- */
-void
-execute_file_actions(filemap_t *filemap)
-{
-	if (datadir_source)
-		copy_executeFileMap(filemap);
-	else
-		libpq_executeFileMap(filemap);
-}
-
-/*
- * Fetch a single file into a malloc'd buffer. The file size is returned
- * in *filesize. The returned buffer is always zero-terminated, which is
- * handy for text files.
- */
-char *
-fetchFile(const char *filename, size_t *filesize)
-{
-	if (datadir_source)
-		return slurpFile(datadir_source, filename, filesize);
-	else
-		return libpqGetFile(filename, filesize);
-}
diff --git a/src/bin/pg_rewind/fetch.h b/src/bin/pg_rewind/fetch.h
deleted file mode 100644
index b20df8b1537..00000000000
--- a/src/bin/pg_rewind/fetch.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*-------------------------------------------------------------------------
- *
- * fetch.h
- *	  Fetching data from a local or remote data directory.
- *
- * This file includes the prototypes for functions used to copy files from
- * one data directory to another. The source to copy from can be a local
- * directory (copy method), or a remote PostgreSQL server (libpq fetch
- * method).
- *
- * Copyright (c) 2013-2020, PostgreSQL Global Development Group
- *
- *-------------------------------------------------------------------------
- */
-#ifndef FETCH_H
-#define FETCH_H
-
-#include "access/xlogdefs.h"
-
-#include "filemap.h"
-
-/*
- * Common interface. Calls the copy or libpq method depending on global
- * config options.
- */
-extern void fetchSourceFileList(void);
-extern char *fetchFile(const char *filename, size_t *filesize);
-extern void execute_file_actions(filemap_t *filemap);
-
-/* in libpq_fetch.c */
-extern void libpqProcessFileList(void);
-extern char *libpqGetFile(const char *filename, size_t *filesize);
-extern void libpq_executeFileMap(filemap_t *map);
-
-extern void libpqConnect(const char *connstr);
-extern XLogRecPtr libpqGetCurrentXlogInsertLocation(void);
-
-/* in copy_fetch.c */
-extern void copy_executeFileMap(filemap_t *map);
-
-typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
-extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
-
-#endif							/* FETCH_H */
diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c
index ec37d0b2e0d..065368a2208 100644
--- a/src/bin/pg_rewind/file_ops.c
+++ b/src/bin/pg_rewind/file_ops.c
@@ -15,6 +15,7 @@
 #include "postgres_fe.h"
 
 #include <sys/stat.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <unistd.h>
 
@@ -35,6 +36,9 @@ static void remove_target_dir(const char *path);
 static void create_target_symlink(const char *path, const char *link);
 static void remove_target_symlink(const char *path);
 
+static void recurse_dir(const char *datadir, const char *parentpath,
+						process_file_callback_t callback);
+
 /*
  * Open a target file for writing. If 'trunc' is true and the file already
  * exists, it will be truncated.
@@ -83,7 +87,7 @@ close_target_file(void)
 void
 write_target_range(char *buf, off_t begin, size_t size)
 {
-	int			writeleft;
+	size_t		writeleft;
 	char	   *p;
 
 	/* update progress report */
@@ -101,7 +105,7 @@ write_target_range(char *buf, off_t begin, size_t size)
 	p = buf;
 	while (writeleft > 0)
 	{
-		int			writelen;
+		ssize_t		writelen;
 
 		errno = 0;
 		writelen = write(dstfd, p, writeleft);
@@ -305,9 +309,6 @@ sync_target_dir(void)
  * buffer is actually *filesize + 1. That's handy when reading a text file.
  * This function can be used to read binary files as well, you can just
  * ignore the zero-terminator in that case.
- *
- * This function is used to implement the fetchFile function in the "fetch"
- * interface (see fetch.c), but is also called directly.
  */
 char *
 slurpFile(const char *datadir, const char *path, size_t *filesize)
@@ -352,3 +353,125 @@ slurpFile(const char *datadir, const char *path, size_t *filesize)
 		*filesize = len;
 	return buffer;
 }
+
+/*
+ * Traverse through all files in a data directory, calling 'callback'
+ * for each file.
+ */
+void
+traverse_datadir(const char *datadir, process_file_callback_t callback)
+{
+	recurse_dir(datadir, NULL, callback);
+}
+
+/*
+ * recursive part of traverse_datadir
+ *
+ * parentpath is the current subdirectory's path relative to datadir,
+ * or NULL at the top level.
+ */
+static void
+recurse_dir(const char *datadir, const char *parentpath,
+			process_file_callback_t callback)
+{
+	DIR		   *xldir;
+	struct dirent *xlde;
+	char		fullparentpath[MAXPGPATH];
+
+	if (parentpath)
+		snprintf(fullparentpath, MAXPGPATH, "%s/%s", datadir, parentpath);
+	else
+		snprintf(fullparentpath, MAXPGPATH, "%s", datadir);
+
+	xldir = opendir(fullparentpath);
+	if (xldir == NULL)
+		pg_fatal("could not open directory \"%s\": %m",
+				 fullparentpath);
+
+	while (errno = 0, (xlde = readdir(xldir)) != NULL)
+	{
+		struct stat fst;
+		char		fullpath[MAXPGPATH * 2];
+		char		path[MAXPGPATH * 2];
+
+		if (strcmp(xlde->d_name, ".") == 0 ||
+			strcmp(xlde->d_name, "..") == 0)
+			continue;
+
+		snprintf(fullpath, sizeof(fullpath), "%s/%s", fullparentpath, xlde->d_name);
+
+		if (lstat(fullpath, &fst) < 0)
+		{
+			if (errno == ENOENT)
+			{
+				/*
+				 * File doesn't exist anymore. This is ok, if the new primary
+				 * is running and the file was just removed. If it was a data
+				 * file, there should be a WAL record of the removal. If it
+				 * was something else, it couldn't have been anyway.
+				 *
+				 * TODO: But complain if we're processing the target dir!
+				 */
+			}
+			else
+				pg_fatal("could not stat file \"%s\": %m",
+						 fullpath);
+		}
+
+		if (parentpath)
+			snprintf(path, sizeof(path), "%s/%s", parentpath, xlde->d_name);
+		else
+			snprintf(path, sizeof(path), "%s", xlde->d_name);
+
+		if (S_ISREG(fst.st_mode))
+			callback(path, FILE_TYPE_REGULAR, fst.st_size, NULL);
+		else if (S_ISDIR(fst.st_mode))
+		{
+			callback(path, FILE_TYPE_DIRECTORY, 0, NULL);
+			/* recurse to handle subdirectories */
+			recurse_dir(datadir, path, callback);
+		}
+#ifndef WIN32
+		else if (S_ISLNK(fst.st_mode))
+#else
+		else if (pgwin32_is_junction(fullpath))
+#endif
+		{
+#if defined(HAVE_READLINK) || defined(WIN32)
+			char		link_target[MAXPGPATH];
+			int			len;
+
+			len = readlink(fullpath, link_target, sizeof(link_target));
+			if (len < 0)
+				pg_fatal("could not read symbolic link \"%s\": %m",
+						 fullpath);
+			if (len >= sizeof(link_target))
+				pg_fatal("symbolic link \"%s\" target is too long",
+						 fullpath);
+			link_target[len] = '\0';
+
+			callback(path, FILE_TYPE_SYMLINK, 0, link_target);
+
+			/*
+			 * If it's a symlink within pg_tblspc, we need to recurse into it,
+			 * to process all the tablespaces.  We also follow a symlink if
+			 * it's for pg_wal.  Symlinks elsewhere are ignored.
+			 */
+			if ((parentpath && strcmp(parentpath, "pg_tblspc") == 0) ||
+				strcmp(path, "pg_wal") == 0)
+				recurse_dir(datadir, path, callback);
+#else
+			pg_fatal("\"%s\" is a symbolic link, but symbolic links are not supported on this platform",
+					 fullpath);
+#endif							/* HAVE_READLINK */
+		}
+	}
+
+	if (errno)
+		pg_fatal("could not read directory \"%s\": %m",
+				 fullparentpath);
+
+	if (closedir(xldir))
+		pg_fatal("could not close directory \"%s\": %m",
+				 fullparentpath);
+}
diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h
index d8466385cf5..c7630859768 100644
--- a/src/bin/pg_rewind/file_ops.h
+++ b/src/bin/pg_rewind/file_ops.h
@@ -23,4 +23,7 @@ extern void sync_target_dir(void);
 
 extern char *slurpFile(const char *datadir, const char *path, size_t *filesize);
 
+typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target);
+extern void traverse_datadir(const char *datadir, process_file_callback_t callback);
+
 #endif							/* FILE_OPS_H */
diff --git a/src/bin/pg_rewind/libpq_fetch.c b/src/bin/pg_rewind/libpq_source.c
similarity index 65%
rename from src/bin/pg_rewind/libpq_fetch.c
rename to src/bin/pg_rewind/libpq_source.c
index 16d451ae167..30294d582ee 100644
--- a/src/bin/pg_rewind/libpq_fetch.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -1,7 +1,7 @@
 /*-------------------------------------------------------------------------
  *
- * libpq_fetch.c
- *	  Functions for fetching files from a remote server.
+ * libpq_source.c
+ *	  Functions for fetching files from a remote server via libpq.
  *
  * Copyright (c) 2013-2020, PostgreSQL Global Development Group
  *
@@ -9,21 +9,14 @@
  */
 #include "postgres_fe.h"
 
-#include <sys/stat.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <unistd.h>
-
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
 #include "datapagemap.h"
-#include "fetch.h"
 #include "file_ops.h"
 #include "filemap.h"
 #include "pg_rewind.h"
 #include "port/pg_bswap.h"
-
-PGconn	   *conn = NULL;
+#include "rewind_source.h"
 
 /*
  * Files are fetched max CHUNKSIZE bytes at a time.
@@ -34,30 +27,71 @@ PGconn	   *conn = NULL;
  */
 #define CHUNKSIZE 1000000
 
-static void receiveFileChunks(const char *sql);
-static void execute_pagemap(datapagemap_t *pagemap, const char *path);
-static char *run_simple_query(const char *sql);
-static void run_simple_command(const char *sql);
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	PGconn	   *conn;
+	bool		copy_started;
+} libpq_source;
+
+static void init_libpq_conn(PGconn *conn);
+static char *run_simple_query(PGconn *conn, const char *sql);
+static void run_simple_command(PGconn *conn, const char *sql);
+
+/* public interface functions */
+static void libpq_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static void libpq_queue_fetch_range(rewind_source *source, const char *path,
+									off_t off, size_t len);
+static void libpq_finish_fetch(rewind_source *source);
+static char *libpq_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static XLogRecPtr libpq_get_current_wal_insert_lsn(rewind_source *source);
+static void libpq_destroy(rewind_source *source);
 
-void
-libpqConnect(const char *connstr)
+/*
+ * Create a new libpq source.
+ *
+ * The caller has already established the connection, but should not try
+ * to use it while the source is active.
+ */
+rewind_source *
+init_libpq_source(PGconn *conn)
 {
-	char	   *str;
-	PGresult   *res;
+	libpq_source *src;
+
+	init_libpq_conn(conn);
+
+	src = pg_malloc0(sizeof(libpq_source));
+
+	src->common.traverse_files = libpq_traverse_files;
+	src->common.fetch_file = libpq_fetch_file;
+	src->common.queue_fetch_range = libpq_queue_fetch_range;
+	src->common.finish_fetch = libpq_finish_fetch;
+	src->common.get_current_wal_insert_lsn = libpq_get_current_wal_insert_lsn;
+	src->common.destroy = libpq_destroy;
 
-	conn = PQconnectdb(connstr);
-	if (PQstatus(conn) == CONNECTION_BAD)
-		pg_fatal("could not connect to server: %s",
-				 PQerrorMessage(conn));
+	src->conn = conn;
 
-	if (showprogress)
-		pg_log_info("connected to server");
+	return &src->common;
+}
+
+/*
+ * Initialize a libpq connection for use.
+ */
+static void
+init_libpq_conn(PGconn *conn)
+{
+	PGresult   *res;
+	char	   *str;
 
 	/* disable all types of timeouts */
-	run_simple_command("SET statement_timeout = 0");
-	run_simple_command("SET lock_timeout = 0");
-	run_simple_command("SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET statement_timeout = 0");
+	run_simple_command(conn, "SET lock_timeout = 0");
+	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
 
+	/* secure search_path */
 	res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not clear search_path: %s",
@@ -70,7 +104,7 @@ libpqConnect(const char *connstr)
 	 * currently because we use a temporary table. Better to check for it
 	 * explicitly than error out, for a better error message.
 	 */
-	str = run_simple_query("SELECT pg_is_in_recovery()");
+	str = run_simple_query(conn, "SELECT pg_is_in_recovery()");
 	if (strcmp(str, "f") != 0)
 		pg_fatal("source server must not be in recovery mode");
 	pg_free(str);
@@ -80,27 +114,19 @@ libpqConnect(const char *connstr)
 	 * a page is modified while we read it with pg_read_binary_file(), and we
 	 * rely on full page images to fix them.
 	 */
-	str = run_simple_query("SHOW full_page_writes");
+	str = run_simple_query(conn, "SHOW full_page_writes");
 	if (strcmp(str, "on") != 0)
 		pg_fatal("full_page_writes must be enabled in the source server");
 	pg_free(str);
-
-	/*
-	 * Although we don't do any "real" updates, we do work with a temporary
-	 * table. We don't care about synchronous commit for that. It doesn't
-	 * otherwise matter much, but if the server is using synchronous
-	 * replication, and replication isn't working for some reason, we don't
-	 * want to get stuck, waiting for it to start working again.
-	 */
-	run_simple_command("SET synchronous_commit = off");
 }
 
 /*
- * Runs a query that returns a single value.
+ * Run a query that returns a single value.
+ *
  * The result should be pg_free'd after use.
  */
 static char *
-run_simple_query(const char *sql)
+run_simple_query(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 	char	   *result;
@@ -123,11 +149,12 @@ run_simple_query(const char *sql)
 }
 
 /*
- * Runs a command.
+ * Run a command.
+ *
  * In the event of a failure, exit immediately.
  */
 static void
-run_simple_command(const char *sql)
+run_simple_command(PGconn *conn, const char *sql)
 {
 	PGresult   *res;
 
@@ -141,17 +168,18 @@ run_simple_command(const char *sql)
 }
 
 /*
- * Calls pg_current_wal_insert_lsn() function
+ * Call the pg_current_wal_insert_lsn() function in the remote system.
  */
-XLogRecPtr
-libpqGetCurrentXlogInsertLocation(void)
+static XLogRecPtr
+libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
 	uint32		hi;
 	uint32		lo;
 	char	   *val;
 
-	val = run_simple_query("SELECT pg_current_wal_insert_lsn()");
+	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
 	if (sscanf(val, "%X/%X", &hi, &lo) != 2)
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
@@ -166,9 +194,10 @@ libpqGetCurrentXlogInsertLocation(void)
 /*
  * Get a list of all files in the data directory.
  */
-void
-libpqProcessFileList(void)
+static void
+libpq_traverse_files(rewind_source *source, process_file_callback_t callback)
 {
+	PGconn *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	const char *sql;
 	int			i;
@@ -246,30 +275,114 @@ libpqProcessFileList(void)
 	PQclear(res);
 }
 
-/*----
- * Runs a query, which returns pieces of files from the remote source data
- * directory, and overwrites the corresponding parts of target files with
- * the received parts. The result set is expected to be of format:
- *
- * path		text	-- path in the data directory, e.g "base/1/123"
- * begin	int8	-- offset within the file
- * chunk	bytea	-- file content
- *----
+/*
+ * Queue up a request to fetch a piece of a file from remote system.
  */
 static void
-receiveFileChunks(const char *sql)
+libpq_queue_fetch_range(rewind_source *source, const char *path, off_t off,
+						size_t len)
 {
+	libpq_source *src = (libpq_source *) source;
+	uint64		begin = off;
+	uint64		end = off + len;
+
+	/*
+	 * On first call, create a temporary table, and start COPYing to it.
+	 * We will load it with the list of blocks that we need to fetch.
+	 */
+	if (!src->copy_started)
+	{
+		PGresult   *res;
+
+		run_simple_command(src->conn, "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4)");
+
+		res = PQexec(src->conn, "COPY fetchchunks FROM STDIN");
+		if (PQresultStatus(res) != PGRES_COPY_IN)
+			pg_fatal("could not send file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+
+		src->copy_started = true;
+	}
+
+	/*
+	 * Write the file range to a temporary table in the server.
+	 *
+	 * The range is sent to the server as a COPY formatted line, to be inserted
+	 * into the 'fetchchunks' temporary table. The libpq_finish_fetch() uses
+	 * the temporary table to actually fetch the data.
+	 */
+
+	/* Split the range into CHUNKSIZE chunks */
+	while (end - begin > 0)
+	{
+		char		linebuf[MAXPGPATH + 23];
+		unsigned int len;
+
+		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
+		if (end - begin > CHUNKSIZE)
+			len = CHUNKSIZE;
+		else
+			len = (unsigned int) (end - begin);
+
+		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
+
+		if (PQputCopyData(src->conn, linebuf, strlen(linebuf)) != 1)
+			pg_fatal("could not send COPY data: %s",
+					 PQerrorMessage(src->conn));
+
+		begin += len;
+	}
+}
+
+/*
+ * Receive all the queued chunks and write them to the target data directory.
+ */
+static void
+libpq_finish_fetch(rewind_source *source)
+{
+	libpq_source *src = (libpq_source *) source;
 	PGresult   *res;
+	const char *sql;
 
-	if (PQsendQueryParams(conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
-		pg_fatal("could not send query: %s", PQerrorMessage(conn));
+	if (PQputCopyEnd(src->conn, NULL) != 1)
+		pg_fatal("could not send end-of-COPY: %s",
+				 PQerrorMessage(src->conn));
+
+	while ((res = PQgetResult(src->conn)) != NULL)
+	{
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("unexpected result while sending file list: %s",
+					 PQresultErrorMessage(res));
+		PQclear(res);
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	sql =
+		"SELECT path, begin,\n"
+		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
+		"FROM fetchchunks\n";
+
+	if (PQsendQueryParams(src->conn, sql, 0, NULL, NULL, NULL, NULL, 1) != 1)
+		pg_fatal("could not send query: %s", PQerrorMessage(src->conn));
 
 	pg_log_debug("getting file chunks");
 
-	if (PQsetSingleRowMode(conn) != 1)
+	if (PQsetSingleRowMode(src->conn) != 1)
 		pg_fatal("could not set libpq connection to single row mode");
 
-	while ((res = PQgetResult(conn)) != NULL)
+	/*----
+	 * The result set is of format:
+	 *
+	 * path		text	-- path in the data directory, e.g "base/1/123"
+	 * begin	int8	-- offset within the file
+	 * chunk	bytea	-- file content
+	 *----
+	 */
+	while ((res = PQgetResult(src->conn)) != NULL)
 	{
 		char	   *filename;
 		int			filenamelen;
@@ -349,8 +462,8 @@ receiveFileChunks(const char *sql)
 			continue;
 		}
 
-		pg_log_debug("received chunk for file \"%s\", offset %lld, size %d",
-					 filename, (long long int) chunkoff, chunksize);
+		pg_log_debug("received chunk for file \"%s\", offset " INT64_FORMAT ", size %d",
+					 filename, chunkoff, chunksize);
 
 		open_target_file(filename, false);
 
@@ -363,28 +476,29 @@ receiveFileChunks(const char *sql)
 }
 
 /*
- * Receive a single file as a malloc'd buffer.
+ * Fetch a single file as a malloc'd buffer.
  */
-char *
-libpqGetFile(const char *filename, size_t *filesize)
+static char *
+libpq_fetch_file(rewind_source *source, const char *path, size_t *filesize)
 {
+	PGconn	   *conn = ((libpq_source *) source)->conn;
 	PGresult   *res;
 	char	   *result;
 	int			len;
 	const char *paramValues[1];
 
-	paramValues[0] = filename;
+	paramValues[0] = path;
 	res = PQexecParams(conn, "SELECT pg_read_binary_file($1)",
 					   1, NULL, paramValues, NULL, NULL, 1);
 
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pg_fatal("could not fetch remote file \"%s\": %s",
-				 filename, PQresultErrorMessage(res));
+				 path, PQresultErrorMessage(res));
 
 	/* sanity check the result set */
 	if (PQntuples(res) != 1 || PQgetisnull(res, 0, 0))
 		pg_fatal("unexpected result set while fetching remote file \"%s\"",
-				 filename);
+				 path);
 
 	/* Read result to local variables */
 	len = PQgetlength(res, 0, 0);
@@ -394,7 +508,7 @@ libpqGetFile(const char *filename, size_t *filesize)
 
 	PQclear(res);
 
-	pg_log_debug("fetched file \"%s\", length %d", filename, len);
+	pg_log_debug("fetched file \"%s\", length %d", path, len);
 
 	if (filesize)
 		*filesize = len;
@@ -402,142 +516,11 @@ libpqGetFile(const char *filename, size_t *filesize)
 }
 
 /*
- * Write a file range to a temporary table in the server.
- *
- * The range is sent to the server as a COPY formatted line, to be inserted
- * into the 'fetchchunks' temporary table. It is used in receiveFileChunks()
- * function to actually fetch the data.
- */
-static void
-fetch_file_range(const char *path, uint64 begin, uint64 end)
-{
-	char		linebuf[MAXPGPATH + 23];
-
-	/* Split the range into CHUNKSIZE chunks */
-	while (end - begin > 0)
-	{
-		unsigned int len;
-
-		/* Fine as long as CHUNKSIZE is not bigger than UINT32_MAX */
-		if (end - begin > CHUNKSIZE)
-			len = CHUNKSIZE;
-		else
-			len = (unsigned int) (end - begin);
-
-		snprintf(linebuf, sizeof(linebuf), "%s\t" UINT64_FORMAT "\t%u\n", path, begin, len);
-
-		if (PQputCopyData(conn, linebuf, strlen(linebuf)) != 1)
-			pg_fatal("could not send COPY data: %s",
-					 PQerrorMessage(conn));
-
-		begin += len;
-	}
-}
-
-/*
- * Fetch all changed blocks from remote source data directory.
+ * Close a libpq source.
  */
-void
-libpq_executeFileMap(filemap_t *map)
-{
-	file_entry_t *entry;
-	const char *sql;
-	PGresult   *res;
-	int			i;
-
-	/*
-	 * First create a temporary table, and load it with the blocks that we
-	 * need to fetch.
-	 */
-	sql = "CREATE TEMPORARY TABLE fetchchunks(path text, begin int8, len int4);";
-	run_simple_command(sql);
-
-	sql = "COPY fetchchunks FROM STDIN";
-	res = PQexec(conn, sql);
-
-	if (PQresultStatus(res) != PGRES_COPY_IN)
-		pg_fatal("could not send file list: %s",
-				 PQresultErrorMessage(res));
-	PQclear(res);
-
-	for (i = 0; i < map->nentries; i++)
-	{
-		entry = map->entries[i];
-
-		/* If this is a relation file, copy the modified blocks */
-		execute_pagemap(&entry->target_pages_to_overwrite, entry->path);
-
-		switch (entry->action)
-		{
-			case FILE_ACTION_NONE:
-				/* nothing else to do */
-				break;
-
-			case FILE_ACTION_COPY:
-				/* Truncate the old file out of the way, if any */
-				open_target_file(entry->path, true);
-				fetch_file_range(entry->path, 0, entry->source_size);
-				break;
-
-			case FILE_ACTION_TRUNCATE:
-				truncate_target_file(entry->path, entry->source_size);
-				break;
-
-			case FILE_ACTION_COPY_TAIL:
-				fetch_file_range(entry->path, entry->target_size, entry->source_size);
-				break;
-
-			case FILE_ACTION_REMOVE:
-				remove_target(entry);
-				break;
-
-			case FILE_ACTION_CREATE:
-				create_target(entry);
-				break;
-
-			case FILE_ACTION_UNDECIDED:
-				pg_fatal("no action decided for \"%s\"", entry->path);
-				break;
-		}
-	}
-
-	if (PQputCopyEnd(conn, NULL) != 1)
-		pg_fatal("could not send end-of-COPY: %s",
-				 PQerrorMessage(conn));
-
-	while ((res = PQgetResult(conn)) != NULL)
-	{
-		if (PQresultStatus(res) != PGRES_COMMAND_OK)
-			pg_fatal("unexpected result while sending file list: %s",
-					 PQresultErrorMessage(res));
-		PQclear(res);
-	}
-
-	/*
-	 * We've now copied the list of file ranges that we need to fetch to the
-	 * temporary table. Now, actually fetch all of those ranges.
-	 */
-	sql =
-		"SELECT path, begin,\n"
-		"  pg_read_binary_file(path, begin, len, true) AS chunk\n"
-		"FROM fetchchunks\n";
-
-	receiveFileChunks(sql);
-}
-
 static void
-execute_pagemap(datapagemap_t *pagemap, const char *path)
+libpq_destroy(rewind_source *source)
 {
-	datapagemap_iterator_t *iter;
-	BlockNumber blkno;
-	off_t		offset;
-
-	iter = datapagemap_iterate(pagemap);
-	while (datapagemap_next(iter, &blkno))
-	{
-		offset = blkno * BLCKSZ;
-
-		fetch_file_range(path, offset, offset + BLCKSZ);
-	}
-	pg_free(iter);
+	pfree(source);
+	/* NOTE: we don't close the connection here, as it was not opened by us. */
 }
diff --git a/src/bin/pg_rewind/local_source.c b/src/bin/pg_rewind/local_source.c
new file mode 100644
index 00000000000..193a412f9ae
--- /dev/null
+++ b/src/bin/pg_rewind/local_source.c
@@ -0,0 +1,134 @@
+/*-------------------------------------------------------------------------
+ *
+ * local_source.c
+ *	  Functions for using a local data directory as the source.
+ *
+ * Portions Copyright (c) 2013-2020, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres_fe.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "datapagemap.h"
+#include "file_ops.h"
+#include "filemap.h"
+#include "pg_rewind.h"
+#include "rewind_source.h"
+
+typedef struct
+{
+	rewind_source common;	/* common interface functions */
+
+	const char *datadir;	/* path to the source data directory */
+} local_source;
+
+static void local_traverse_files(rewind_source *source,
+								 process_file_callback_t callback);
+static char *local_fetch_file(rewind_source *source, const char *path,
+							  size_t *filesize);
+static void local_fetch_file_range(rewind_source *source, const char *path,
+								   off_t off, size_t len);
+static void local_finish_fetch(rewind_source *source);
+static void local_destroy(rewind_source *source);
+
+rewind_source *
+init_local_source(const char *datadir)
+{
+	local_source *src;
+
+	src = pg_malloc0(sizeof(local_source));
+
+	src->common.traverse_files = local_traverse_files;
+	src->common.fetch_file = local_fetch_file;
+	src->common.queue_fetch_range = local_fetch_file_range;
+	src->common.finish_fetch = local_finish_fetch;
+	src->common.get_current_wal_insert_lsn = NULL;
+	src->common.destroy = local_destroy;
+
+	src->datadir = datadir;
+
+	return &src->common;
+}
+
+static void
+local_traverse_files(rewind_source *source, process_file_callback_t callback)
+{
+	traverse_datadir(((local_source *) source)->datadir, &process_source_file);
+}
+
+static char *
+local_fetch_file(rewind_source *source, const char *path, size_t *filesize)
+{
+	return slurpFile(((local_source *) source)->datadir, path, filesize);
+}
+
+/*
+ * Copy a file from source to target, starting at 'off', for 'len' bytes.
+ *
+ * If 'trunc' is true, any existing file with the same name is truncated.
+ */
+static void
+local_fetch_file_range(rewind_source *source, const char *path, off_t off,
+					   size_t len)
+{
+	const char *datadir = ((local_source *) source)->datadir;
+	PGAlignedBlock buf;
+	char		srcpath[MAXPGPATH];
+	int			srcfd;
+	off_t		begin = off;
+	off_t		end = off + len;
+
+	snprintf(srcpath, sizeof(srcpath), "%s/%s", datadir, path);
+
+	srcfd = open(srcpath, O_RDONLY | PG_BINARY, 0);
+	if (srcfd < 0)
+		pg_fatal("could not open source file \"%s\": %m",
+				 srcpath);
+
+	if (lseek(srcfd, begin, SEEK_SET) == -1)
+		pg_fatal("could not seek in source file: %m");
+
+	open_target_file(path, false);
+
+	while (end - begin > 0)
+	{
+		ssize_t		readlen;
+		size_t		len;
+
+		if (end - begin > sizeof(buf))
+			len = sizeof(buf);
+		else
+			len = end - begin;
+
+		readlen = read(srcfd, buf.data, len);
+
+		if (readlen < 0)
+			pg_fatal("could not read file \"%s\": %m", srcpath);
+		else if (readlen == 0)
+			pg_fatal("unexpected EOF while reading file \"%s\"", srcpath);
+
+		write_target_range(buf.data, begin, readlen);
+		begin += readlen;
+	}
+
+	if (close(srcfd) != 0)
+		pg_fatal("could not close file \"%s\": %m", srcpath);
+}
+
+static void
+local_finish_fetch(rewind_source *source)
+{
+	/*
+	 * Nothing to do, local_fetch_file_range() performs the fetching
+	 * immediately.
+	 */
+}
+
+static void
+local_destroy(rewind_source *source)
+{
+	pfree(source);
+}
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 574d7f7163b..33042035424 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -23,20 +23,25 @@
 #include "common/restricted_token.h"
 #include "common/string.h"
 #include "fe_utils/recovery_gen.h"
-#include "fetch.h"
 #include "file_ops.h"
 #include "filemap.h"
 #include "getopt_long.h"
 #include "pg_rewind.h"
+#include "rewind_source.h"
 #include "storage/bufpage.h"
 
 static void usage(const char *progname);
 
+static void perform_rewind(filemap_t *filemap, rewind_source *source,
+						   XLogRecPtr chkptrec,
+						   TimeLineID chkpttli,
+						   XLogRecPtr chkptredo);
+
 static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
 							  XLogRecPtr checkpointloc);
 
-static void digestControlFile(ControlFileData *ControlFile, char *source,
-							  size_t size);
+static void digestControlFile(ControlFileData *ControlFile,
+							  const char *content, size_t size);
 static void getRestoreCommand(const char *argv0);
 static void sanityChecks(void);
 static void findCommonAncestorTimeline(XLogRecPtr *recptr, int *tliIndex);
@@ -69,6 +74,8 @@ int			targetNentries;
 uint64		fetch_size;
 uint64		fetch_done;
 
+static PGconn *conn;
+static rewind_source *source;
 
 static void
 usage(const char *progname)
@@ -125,9 +132,6 @@ main(int argc, char **argv)
 	char	   *buffer;
 	bool		no_ensure_shutdown = false;
 	bool		rewind_needed;
-	XLogRecPtr	endrec;
-	TimeLineID	endtli;
-	ControlFileData ControlFile_new;
 	bool		writerecoveryconf = false;
 	filemap_t  *filemap;
 
@@ -269,19 +273,29 @@ main(int argc, char **argv)
 
 	atexit(disconnect_atexit);
 
-	/* Connect to remote server */
-	if (connstr_source)
-		libpqConnect(connstr_source);
-
 	/*
-	 * Ok, we have all the options and we're ready to start. Read in all the
-	 * information we need from both clusters.
+	 * Ok, we have all the options and we're ready to start. First, connect
+	 * to remote server.
 	 */
-	buffer = slurpFile(datadir_target, "global/pg_control", &size);
-	digestControlFile(&ControlFile_target, buffer, size);
-	pg_free(buffer);
+	if (connstr_source)
+	{
+		conn = PQconnectdb(connstr_source);
+
+		if (PQstatus(conn) == CONNECTION_BAD)
+			pg_fatal("could not connect to server: %s",
+					 PQerrorMessage(conn));
+
+		if (showprogress)
+			pg_log_info("connected to server");
+
+		source = init_libpq_source(conn);
+	}
+	else
+		source = init_local_source(datadir_source);
 
 	/*
+	 * Check the status of the target instance.
+	 *
 	 * If the target instance was not cleanly shut down, start and stop the
 	 * target cluster once in single-user mode to enforce recovery to finish,
 	 * ensuring that the cluster can be used by pg_rewind.  Note that if
@@ -289,6 +303,10 @@ main(int argc, char **argv)
 	 * need to make sure by themselves that the target cluster is in a clean
 	 * state.
 	 */
+	buffer = slurpFile(datadir_target, "global/pg_control", &size);
+	digestControlFile(&ControlFile_target, buffer, size);
+	pg_free(buffer);
+
 	if (!no_ensure_shutdown &&
 		ControlFile_target.state != DB_SHUTDOWNED &&
 		ControlFile_target.state != DB_SHUTDOWNED_IN_RECOVERY)
@@ -300,17 +318,20 @@ main(int argc, char **argv)
 		pg_free(buffer);
 	}
 
-	buffer = fetchFile("global/pg_control", &size);
+	buffer = source->fetch_file(source, "global/pg_control", &size);
 	digestControlFile(&ControlFile_source, buffer, size);
 	pg_free(buffer);
 
 	sanityChecks();
 
 	/*
+	 * Find the common ancestor timeline between the clusters.
+	 *
 	 * If both clusters are already on the same timeline, there's nothing to
 	 * do.
 	 */
-	if (ControlFile_target.checkPointCopy.ThisTimeLineID == ControlFile_source.checkPointCopy.ThisTimeLineID)
+	if (ControlFile_target.checkPointCopy.ThisTimeLineID ==
+		ControlFile_source.checkPointCopy.ThisTimeLineID)
 	{
 		pg_log_info("source and target cluster are on the same timeline");
 		rewind_needed = false;
@@ -373,11 +394,11 @@ main(int argc, char **argv)
 	filehash_init();
 
 	/*
-	 * Collect information about all files in the target and source systems.
+	 * Collect information about all files in the both data directories.
 	 */
 	if (showprogress)
 		pg_log_info("reading source file list");
-	fetchSourceFileList();
+	source->traverse_files(source, &process_source_file);
 
 	if (showprogress)
 		pg_log_info("reading target file list");
@@ -421,11 +442,124 @@ main(int argc, char **argv)
 	}
 
 	/*
-	 * This is the point of no return. Once we start copying things, we have
-	 * modified the target directory and there is no turning back!
+	 * We have now collected all the information we need from both systems,
+	 * and we are ready to start modifying the target directory.
+	 *
+	 * This is the point of no return. Once we start copying things, there is
+	 * no turning back!
 	 */
+	perform_rewind(filemap, source, chkptrec, chkpttli, chkptredo);
 
-	execute_file_actions(filemap);
+	if (showprogress)
+		pg_log_info("syncing target data directory");
+	sync_target_dir();
+
+	/* Also update the standby configuration, if requested. */
+	if (writerecoveryconf && !dry_run)
+		WriteRecoveryConfig(conn, datadir_target,
+							GenerateRecoveryConfig(conn, NULL));
+
+	/* don't need the source connection anymore */
+	source->destroy(source);
+	if (conn)
+	{
+		PQfinish(conn);
+		conn = NULL;
+	}
+
+	pg_log_info("Done!");
+
+	return 0;
+}
+
+/*
+ * Perform the rewind.
+ *
+ * We have already collected all the information we need from the
+ * target and the source.
+ */
+static void
+perform_rewind(filemap_t *filemap, rewind_source *source,
+			   XLogRecPtr chkptrec,
+			   TimeLineID chkpttli,
+			   XLogRecPtr chkptredo)
+{
+	XLogRecPtr	endrec;
+	TimeLineID	endtli;
+	ControlFileData ControlFile_new;
+
+	/*
+	 * Execute the actions in the file map, fetching data from the source
+	 * system as needed.
+	 */
+	for (int i = 0; i < filemap->nentries; i++)
+	{
+		file_entry_t *entry = filemap->entries[i];
+
+		/*
+		 * If this is a relation file, copy the modified blocks.
+		 *
+		 * This is in addition to any other changes.
+		 */
+		if (entry->target_pages_to_overwrite.bitmapsize > 0)
+		{
+			datapagemap_iterator_t *iter;
+			BlockNumber blkno;
+			off_t		offset;
+
+			iter = datapagemap_iterate(&entry->target_pages_to_overwrite);
+			while (datapagemap_next(iter, &blkno))
+			{
+				offset = blkno * BLCKSZ;
+				source->queue_fetch_range(source, entry->path, offset, BLCKSZ);
+			}
+			pg_free(iter);
+		}
+
+		switch (entry->action)
+		{
+			case FILE_ACTION_NONE:
+				/* nothing else to do */
+				break;
+
+			case FILE_ACTION_COPY:
+				/* Truncate the old file out of the way, if any */
+				open_target_file(entry->path, true);
+				source->queue_fetch_range(source, entry->path,
+										  0, entry->source_size);
+				break;
+
+			case FILE_ACTION_TRUNCATE:
+				truncate_target_file(entry->path, entry->source_size);
+				break;
+
+			case FILE_ACTION_COPY_TAIL:
+				source->queue_fetch_range(source, entry->path,
+										  entry->target_size,
+										  entry->source_size - entry->target_size);
+				break;
+
+			case FILE_ACTION_REMOVE:
+				remove_target(entry);
+				break;
+
+			case FILE_ACTION_CREATE:
+				create_target(entry);
+				break;
+
+			case FILE_ACTION_UNDECIDED:
+				pg_fatal("no action decided for \"%s\"", entry->path);
+				break;
+		}
+	}
+
+	/*
+	 * We've now copied the list of file ranges that we need to fetch to the
+	 * temporary table. Now, actually fetch all of those ranges.
+	 */
+	source->finish_fetch(source);
+
+	close_target_file();
 
 	progress_report(true);
 
@@ -445,7 +579,7 @@ main(int argc, char **argv)
 
 	if (connstr_source)
 	{
-		endrec = libpqGetCurrentXlogInsertLocation();
+		endrec = source->get_current_wal_insert_lsn(source);
 		endtli = ControlFile_source.checkPointCopy.ThisTimeLineID;
 	}
 	else
@@ -458,18 +592,6 @@ main(int argc, char **argv)
 	ControlFile_new.state = DB_IN_ARCHIVE_RECOVERY;
 	if (!dry_run)
 		update_controlfile(datadir_target, &ControlFile_new, do_sync);
-
-	if (showprogress)
-		pg_log_info("syncing target data directory");
-	sync_target_dir();
-
-	if (writerecoveryconf && !dry_run)
-		WriteRecoveryConfig(conn, datadir_target,
-							GenerateRecoveryConfig(conn, NULL));
-
-	pg_log_info("Done!");
-
-	return 0;
 }
 
 static void
@@ -629,7 +751,7 @@ getTimelineHistory(ControlFileData *controlFile, int *nentries)
 
 		/* Get history file from appropriate source */
 		if (controlFile == &ControlFile_source)
-			histfile = fetchFile(path, NULL);
+			histfile = source->fetch_file(source, path, NULL);
 		else if (controlFile == &ControlFile_target)
 			histfile = slurpFile(datadir_target, path, NULL);
 		else
@@ -785,16 +907,17 @@ checkControlFile(ControlFileData *ControlFile)
 }
 
 /*
- * Verify control file contents in the buffer src, and copy it to *ControlFile.
+ * Verify control file contents in the buffer 'content', and copy it to *ControlFile.
  */
 static void
-digestControlFile(ControlFileData *ControlFile, char *src, size_t size)
+digestControlFile(ControlFileData *ControlFile,
+				  const char *content, size_t size)
 {
 	if (size != PG_CONTROL_FILE_SIZE)
 		pg_fatal("unexpected control file size %d, expected %d",
 				 (int) size, PG_CONTROL_FILE_SIZE);
 
-	memcpy(ControlFile, src, sizeof(ControlFileData));
+	memcpy(ControlFile, content, sizeof(ControlFileData));
 
 	/* set and validate WalSegSz */
 	WalSegSz = ControlFile->xlog_seg_size;
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 67f90c2a38c..0dc3dbd5255 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -20,8 +20,6 @@
 
 /* Configuration options */
 extern char *datadir_target;
-extern char *datadir_source;
-extern char *connstr_source;
 extern bool showprogress;
 extern bool dry_run;
 extern bool do_sync;
@@ -31,9 +29,6 @@ extern int	WalSegSz;
 extern TimeLineHistoryEntry *targetHistory;
 extern int	targetNentries;
 
-/* general state */
-extern PGconn *conn;
-
 /* Progress counters */
 extern uint64 fetch_size;
 extern uint64 fetch_done;
diff --git a/src/bin/pg_rewind/rewind_source.h b/src/bin/pg_rewind/rewind_source.h
new file mode 100644
index 00000000000..ec1c160a464
--- /dev/null
+++ b/src/bin/pg_rewind/rewind_source.h
@@ -0,0 +1,79 @@
+/*-------------------------------------------------------------------------
+ *
+ * rewind_source.h
+ *	  Abstraction for fetching from source server.
+ *
+ * The source server can be either a libpq connection to a live system, or
+ * a local data directory. The 'rewind_source' struct abstracts the
+ * operations to fetch data from the source system, so that the rest of
+ * the code doesn't need to care what kind of a source its dealing with.
+ *
+ * Copyright (c) 2013-2020, PostgreSQL Global Development Group
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef REWIND_SOURCE_H
+#define REWIND_SOURCE_H
+
+#include "access/xlogdefs.h"
+#include "file_ops.h"
+#include "filemap.h"
+#include "libpq-fe.h"
+
+typedef struct rewind_source
+{
+	/*
+	 * Traverse all files in the source data directory, and call 'callback'
+	 * on each file.
+	 */
+	void (*traverse_files) (struct rewind_source *,
+							process_file_callback_t callback);
+
+	/*
+	 * Fetch a single file into a malloc'd buffer. The file size is returned
+	 * in *filesize. The returned buffer is always zero-terminated, which is
+	 * handy for text files.
+	 */
+	char *(*fetch_file) (struct rewind_source *, const char *path,
+						 size_t *filesize);
+
+	/*
+	 * Request to fetch (part of) a file in the source system, specified by an
+	 * offset and length, and write it to the same offset in the corresponding
+	 * target file. The source implementation may queue up the request and
+	 * execute it later when convenient. Call finish_fetch() to flush the
+	 * queue and execute all requests.
+	 */
+	void (*queue_fetch_range) (struct rewind_source *, const char *path,
+							   off_t offset, size_t len);
+
+	/*
+	 * Execute all requests queued up with queue_fetch_range().
+	 */
+	void (*finish_fetch) (struct rewind_source *);
+
+	/*
+	 * Get the current WAL insert position in the source system.
+	 */
+	XLogRecPtr (*get_current_wal_insert_lsn) (struct rewind_source *);
+
+	/*
+	 * Free this rewind_source object.
+	 */
+	void (*destroy) (struct rewind_source *);
+
+} rewind_source;
+
+
+/*
+ * Execute all the actions in 'filemap'.
+ */
+extern void execute_file_actions(filemap_t *filemap, rewind_source *source);
+
+/* in libpq_source.c */
+extern rewind_source *init_libpq_source(PGconn *conn);
+
+/* in local_source.c */
+extern rewind_source *init_local_source(const char *datadir);
+
+#endif							/* FETCH_H */
-- 
2.20.1


--------------E5E97ECB089CAF17EDB11AD4
Content-Type: text/x-patch; charset=UTF-8;
 name="v3-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v3-0005-Allow-pg_rewind-to-use-a-standby-server-as-the-so.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* Re: Extending SMgrRelation lifetimes
@ 2023-09-18 15:19  Robert Haas <[email protected]>
  0 siblings, 1 reply; 43+ messages in thread

From: Robert Haas @ 2023-09-18 15:19 UTC (permalink / raw)
  To: Thomas Munro <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; pgsql-hackers

I think that if you believe 0001 to be correct you should go ahead and
commit it sooner rather than later. If you're wrong and something
weird starts happening we'll then have a chance to notice that before
too much other stuff gets changed on top of this and confuses the
matter.

On Wed, Aug 23, 2023 at 12:55 AM Thomas Munro <[email protected]> wrote:
> Right, let's one find one good place.  I think smgropen() would be best.

I think it would be a good idea to give this comment a bit more oomph.
In lieu of this:

+ * This does not attempt to actually open the underlying files.  The returned
+ * object remains valid at least until AtEOXact_SMgr() is called, or until
+ * smgrdestroy() is called in non-transactional backends.

I would leave the existing "This does not attempt to actually open the
underlying files." comment as a separate comment, and add something
like this as a new paragraph:

In versions of PostgreSQL prior to 17, this function returned an
object with no defined lifetime. Now, however, the object remains
valid for the lifetime of the transaction, up to the point where
AtEOXact_SMgr() is called, making it much easier for callers to know
for how long they can hold on to a pointer to the returned object. If
this function is called outside of a transaction, the object remains
valid until smgrdestroy() or smgrdestroyall() is called. Background
processes that use smgr but not transactions typically do this once
per checkpoint cycle.

Apart from that, the main thing that is bothering me is that the
justification for redefining smgrclose() to do what smgrrelease() now
does isn't really spelled out anywhere. You mentioned some reasons and
Heikki mentioned the benefit to extensions, but I feel like somebody
should be able to understand the reasoning clearly from reading the
commit message or the comments in the patch, rather than having to
visit the mailing list discussion, and I'm not sure we're there yet. I
feel like I understood why we were doing this and was convinced it was
a good idea at some point, but now the reasoning has gone out of my
head and I can't recover it. If somebody does smgropen() .. stuff ...
smgrclose() as in heapam_relation_copy_data() or index_copy_data(),
this change has the effect of making the SmgrRelation remain valid
until eoxact whereas before it would have been destroyed instantly. Is
that what we want? Presumably yes, or this patch wouldn't be shaped
like this, but I don't know why we want that...

Another thing that seems a bit puzzling is how this is intended to, or
does, interact with the ownership mechanism. Intuitively, I feel like
a Relation owns an SMgrRelation *because* the SMgrRelation has no
defined lifetime. But I think that's not quite right. I guess the
ownership mechanism doesn't guarantee anything about the lifetime of
the object, just that the pointer in the Relation won't hang around
any longer than the object to which it's pointing. So does that mean
that we're free to redefine the object lifetime to be pretty much
anything we want and that mechanism doesn't really care? Huh,
interesting.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* Re: Extending SMgrRelation lifetimes
@ 2023-11-29 12:41  Heikki Linnakangas <[email protected]>
  parent: Robert Haas <[email protected]>
  0 siblings, 1 reply; 43+ messages in thread

From: Heikki Linnakangas @ 2023-11-29 12:41 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; Thomas Munro <[email protected]>; +Cc: pgsql-hackers

I spent some more time digging into this, experimenting with different 
approaches. Came up with pretty significant changes; see below:

On 18/09/2023 18:19, Robert Haas wrote:
> I think that if you believe 0001 to be correct you should go ahead and
> commit it sooner rather than later. If you're wrong and something
> weird starts happening we'll then have a chance to notice that before
> too much other stuff gets changed on top of this and confuses the
> matter.

+1

> On Wed, Aug 23, 2023 at 12:55 AM Thomas Munro <[email protected]> wrote:
>> Right, let's one find one good place.  I think smgropen() would be best.
> 
> I think it would be a good idea to give this comment a bit more oomph.
> In lieu of this:
> 
> + * This does not attempt to actually open the underlying files.  The returned
> + * object remains valid at least until AtEOXact_SMgr() is called, or until
> + * smgrdestroy() is called in non-transactional backends.
> 
> I would leave the existing "This does not attempt to actually open the
> underlying files." comment as a separate comment, and add something
> like this as a new paragraph:
> 
> In versions of PostgreSQL prior to 17, this function returned an
> object with no defined lifetime. Now, however, the object remains
> valid for the lifetime of the transaction, up to the point where
> AtEOXact_SMgr() is called, making it much easier for callers to know
> for how long they can hold on to a pointer to the returned object. If
> this function is called outside of a transaction, the object remains
> valid until smgrdestroy() or smgrdestroyall() is called. Background
> processes that use smgr but not transactions typically do this once
> per checkpoint cycle.

+1

> Apart from that, the main thing that is bothering me is that the
> justification for redefining smgrclose() to do what smgrrelease() now
> does isn't really spelled out anywhere. You mentioned some reasons and
> Heikki mentioned the benefit to extensions, but I feel like somebody
> should be able to understand the reasoning clearly from reading the
> commit message or the comments in the patch, rather than having to
> visit the mailing list discussion, and I'm not sure we're there yet. I
> feel like I understood why we were doing this and was convinced it was
> a good idea at some point, but now the reasoning has gone out of my
> head and I can't recover it. If somebody does smgropen() .. stuff ...
> smgrclose() as in heapam_relation_copy_data() or index_copy_data(),
> this change has the effect of making the SmgrRelation remain valid
> until eoxact whereas before it would have been destroyed instantly. Is
> that what we want? Presumably yes, or this patch wouldn't be shaped
> like this, but I don't know why we want that...

Fair. I tried to address that by adding an overview comment at top of 
smgr.c, explaining how this stuff work. I hope that helps.

> Another thing that seems a bit puzzling is how this is intended to, or
> does, interact with the ownership mechanism. Intuitively, I feel like
> a Relation owns an SMgrRelation *because* the SMgrRelation has no
> defined lifetime. But I think that's not quite right. I guess the
> ownership mechanism doesn't guarantee anything about the lifetime of
> the object, just that the pointer in the Relation won't hang around
> any longer than the object to which it's pointing. So does that mean
> that we're free to redefine the object lifetime to be pretty much
> anything we want and that mechanism doesn't really care? Huh,
> interesting.

Yeah that owner mechanism is weird. It guarantees that the pointer to 
the SMgrRelation is cleared when the SMgrRelation is destroyed. But it 
also prevents the SMgrRelation from being destroyed at end of 
transaction. That's how it is in 'master' too.

But with this patch, we don't normally call smgrdestroy() on an 
SMgrRelation that came from the relation cache. We do call 
smgrdestroyall() in the aux processes, but they don't have a relcache. 
So the real effect of setting the owner now is to prevent the 
SMgrRelation from being destroyed at end of transaction; the mechanism 
of clearing the pointer is unused.

I found two exceptions to that, though, by adding some extra assertions 
and running the regression tests:

1. The smgrdestroyall() in a single-user backend in RequestCheckpoint(). 
It destroys SMgrRelations belonging to relcache entries, and the owner 
mechanism clears the pointers from the relcache. I think smgrcloseall(), 
or doing nothing, would actually be more appropriate there.

2. A funny case with foreign tables: ANALYZE on a foreign table calls 
visibilitymap_count(). A foreign table has no visibility map so it 
returns 0, but before doing so it calls RelationGetSmgr on the foreign 
table, which has 0/0/0 rellocator. That creates an SMgrRelation for 
0/0/0, and sets the foreign table's relcache entry as its owner. If you 
then call ANALYZE on another foreign table, it also calls 
RelationGetSmgr with 0/0/0 rellocator, returning the same SMgrRelation 
entry, and changes its owner to the new relcache entry. That doesn't 
make much sense and it's pretty accidental that it works at all, so 
attached is a patch to avoid calling visibilitymap_count() on foreign 
tables.

I propose that we replace the single owner with a "pin counter". One 
SMgrRelation can have more than one pin on it, and the guarantee is that 
as long as the pin counter is non-zero, the SMgrRelation cannot be 
destroyed and the pointer remains valid. We don't really need the 
capability for more than one pin at the moment (the regression tests 
pass with an extra assertion that pincount <= 1 after fixing the foreign 
table issue), but it seems more straightforward than tracking an owner.

Here's another reason to do that: I noticed this at the end of 
swap_relation_files():

> 	/*
> 	 * Close both relcache entries' smgr links.  We need this kluge because
> 	 * both links will be invalidated during upcoming CommandCounterIncrement.
> 	 * Whichever of the rels is the second to be cleared will have a dangling
> 	 * reference to the other's smgr entry.  Rather than trying to avoid this
> 	 * by ordering operations just so, it's easiest to close the links first.
> 	 * (Fortunately, since one of the entries is local in our transaction,
> 	 * it's sufficient to clear out our own relcache this way; the problem
> 	 * cannot arise for other backends when they see our update on the
> 	 * non-transient relation.)
> 	 *
> 	 * Caution: the placement of this step interacts with the decision to
> 	 * handle toast rels by recursion.  When we are trying to rebuild pg_class
> 	 * itself, the smgr close on pg_class must happen after all accesses in
> 	 * this function.
> 	 */
> 	RelationCloseSmgrByOid(r1);
> 	RelationCloseSmgrByOid(r2);

If RelationCloseSmgrByOid() merely closes the underlying file descriptor 
but doesn't destroy the SMgrRelation object - as it does with these 
patches - I think we reintroduce the dangling reference problem that the 
comment mentions. But if we allow the same SMgrRelation to be pinned by 
two different relcache entries, the problem goes away and we can remove 
that kluge.

I think we're missing test coverage for that though. I commented out 
those calls in 'master' and ran the regression tests, but got no 
failures. I don't fully understand the problem anyway. Or does it not 
exist anymore? Is there a moment where we have two relcache entries 
pointing to the same SMgrRelation? I don't see it. In any case, with a 
pin counter mechanism, I believe it would be fine.


Summary of the changes to the attached main patch:

* Added an overview comment at top of smgr.c

* Added the comment Robert suggested to smgropen()

* Replaced the single owner with a pin count and smgrpin() / smgrunpin() 
functions. smgrdestroyall() now only destroys unpinned entries

* Removed that kluge from swap_relation_files(). It should not be needed 
anymore with the pin counter.

* Changed a few places in bufmgr.c where we called RelationGetSmgr() on 
every smgr call to keep the SMgrRelation in a local variable. That was 
not safe before, but is now. I don't think we should go on a spree to 
change all callers - RelationGetSmgr() is still cheap - but in these few 
places it seems worthwhile.

* I kept the separate smgrclose() and smgrrelease() functions. I know I 
suggested to just change smgrclose() to do what smgrrelease() did, but 
on second thoughts keeping them separate seems nicer. However, 
sgmgrclose() just calls smgrrelease() now, so the distinction is just 
pro forma. The idea is that if you call smgrclose(), you hint that you 
don't need that SMgrRelation pointer anymore, although there might be 
other pointers to the same object and they stay valid.

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] 0001-Remove-some-obsolete-smgrcloseall-calls.patch (2.8K, ../../[email protected]/2-0001-Remove-some-obsolete-smgrcloseall-calls.patch)
  download | inline diff:
From 23530bcdd36ba7dafbaa7aaf29890dc528d07fad Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email protected]>
Date: Wed, 23 Aug 2023 14:38:38 +1200
Subject: [PATCH 1/3] Remove some obsolete smgrcloseall() calls.

Before the advent of PROCSIGNAL_BARRIER_SMGRRELEASE, we didn't have a
comprehensive way to deal with Windows file handles that get in the way
of unlinking directories.  We had smgrcloseall() calls in a few places
to try to mitigate.

It's still a good idea for bgwriter and checkpointer to do that once per
checkpoint so they don't accumulate unbounded SMgrRelation objects, but
there is no longer any reason to close them at other random places such
as the error path, and the explanation as given in the comments is now
obsolete.

Discussion: https://postgr.es/m/message-id/CA%2BhUKGJ8NTvqLHz6dqbQnt2c8XCki4r2QvXjBQcXpVwxTY_pvA%40mail.gmail.com
---
 src/backend/postmaster/bgwriter.c     | 7 -------
 src/backend/postmaster/checkpointer.c | 7 -------
 src/backend/postmaster/walwriter.c    | 7 -------
 3 files changed, 21 deletions(-)

diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c
index d02dc17b9c1..e14c88ac060 100644
--- a/src/backend/postmaster/bgwriter.c
+++ b/src/backend/postmaster/bgwriter.c
@@ -197,13 +197,6 @@ BackgroundWriterMain(void)
 		 */
 		pg_usleep(1000000L);
 
-		/*
-		 * Close all open files after any error.  This is helpful on Windows,
-		 * where holding deleted files open causes various strange errors.
-		 * It's not clear we need it elsewhere, but shouldn't hurt.
-		 */
-		smgrcloseall();
-
 		/* Report wait end here, when there is no further possibility of wait */
 		pgstat_report_wait_end();
 	}
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 42c807d3528..89ab97bdbc7 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -301,13 +301,6 @@ CheckpointerMain(void)
 		 * fast as we can.
 		 */
 		pg_usleep(1000000L);
-
-		/*
-		 * Close all open files after any error.  This is helpful on Windows,
-		 * where holding deleted files open causes various strange errors.
-		 * It's not clear we need it elsewhere, but shouldn't hurt.
-		 */
-		smgrcloseall();
 	}
 
 	/* We can now handle ereport(ERROR) */
diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c
index 48bc92205b5..8789ffb01d0 100644
--- a/src/backend/postmaster/walwriter.c
+++ b/src/backend/postmaster/walwriter.c
@@ -189,13 +189,6 @@ WalWriterMain(void)
 		 * fast as we can.
 		 */
 		pg_usleep(1000000L);
-
-		/*
-		 * Close all open files after any error.  This is helpful on Windows,
-		 * where holding deleted files open causes various strange errors.
-		 * It's not clear we need it elsewhere, but shouldn't hurt.
-		 */
-		smgrcloseall();
 	}
 
 	/* We can now handle ereport(ERROR) */
-- 
2.39.2



  [text/x-patch] 0002-Don-t-try-to-open-visibilitymap-when-analyzing-a-for.patch (1.4K, ../../[email protected]/3-0002-Don-t-try-to-open-visibilitymap-when-analyzing-a-for.patch)
  download | inline diff:
From 7892cb9642286b81cdb45526f8d23117ed892578 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Wed, 29 Nov 2023 14:07:52 +0200
Subject: [PATCH 2/3] Don't try to open visibilitymap when analyzing a foreign
 table.

Also add an assertion in smgropen() that the relnumber is valid.
---
 src/backend/commands/analyze.c  | 5 ++++-
 src/backend/storage/smgr/smgr.c | 2 ++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index e264ffdcf28..1f4a9516814 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -634,7 +634,10 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 	{
 		BlockNumber relallvisible;
 
-		visibilitymap_count(onerel, &relallvisible, NULL);
+		if (RELKIND_HAS_STORAGE(onerel->rd_rel->relkind))
+			visibilitymap_count(onerel, &relallvisible, NULL);
+		else
+			relallvisible = 0;
 
 		/* Update pg_class for table relation */
 		vac_update_relstats(onerel,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 5d0f3d515c3..4c552649336 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -153,6 +153,8 @@ smgropen(RelFileLocator rlocator, BackendId backend)
 	SMgrRelation reln;
 	bool		found;
 
+	Assert(RelFileNumberIsValid(rlocator.relNumber));
+
 	if (SMgrRelationHash == NULL)
 	{
 		/* First time through: initialize the hash table */
-- 
2.39.2



  [text/x-patch] 0003-Give-SMgrRelation-pointers-a-well-defined-lifetime.patch (29.7K, ../../[email protected]/4-0003-Give-SMgrRelation-pointers-a-well-defined-lifetime.patch)
  download | inline diff:
From 0c62df4fa2be891d61e380b354e6df6c7f3c5aff Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email protected]>
Date: Thu, 10 Aug 2023 18:16:31 +1200
Subject: [PATCH 3/3] Give SMgrRelation pointers a well-defined lifetime.

After calling smgropen(), it was not clear how long you could continue
to use the result, because various code paths including cache
invalidation could call smgrclose(), which freed the memory.

Guarantee that the object won't be destroyed until the end of the
current transaction, or in recovery, the commit/abort record that
destroys the underlying storage.

smgrclose() is now just an alias for smgrrelease(). It closes files
and forgets all state except the rlocator, but keeps the SMgrRelation
object valid.

A new smgrdestroy() function is used by rare places that know there
should be no other references to the SMgrRelation.

The short version:

 * smgrclose() is now just an alias for smgrrelease(). It releases
   resources, but doesn't destroy until EOX
 * smgrdestroy() now frees memory, and should rarely be used.

Existing code should be unaffected, but it is now possible for code that
has an SMgrRelation object to use it repeatedly during a transaction as
long as the storage hasn't been physically dropped.  Such code would
normally hold a lock on the relation.

This also replaces the "ownership" mechanism of SMgrRelations with a
pin counter.  An SMgrRelation can now be "pinned", which prevents it
from being destroyed at end of transaction.  There can be multiple pins
on the same SMgrRelation.  In practice, the pin mechanism is only used
by the relcache, so there cannot be more than one pin on the same
SMgrRelation.  Except with swap_relation_files XXX

Author: Thomas Munro, Heikki Linnakangas
Reviewed-by: Robert Haas <[email protected]>
Discussion: https://postgr.es/m/CA%2BhUKGJ8NTvqLHz6dqbQnt2c8XCki4r2QvXjBQcXpVwxTY_pvA%40mail.gmail.com
---
 src/backend/access/transam/xlogutils.c    |  15 +-
 src/backend/commands/cluster.c            |  19 --
 src/backend/postmaster/bgwriter.c         |   8 +-
 src/backend/postmaster/checkpointer.c     |  15 +-
 src/backend/storage/buffer/bufmgr.c       |  32 +--
 src/backend/storage/freespace/freespace.c |   9 +-
 src/backend/storage/smgr/smgr.c           | 234 ++++++++++++----------
 src/backend/utils/cache/inval.c           |   2 +-
 src/backend/utils/cache/relcache.c        |  21 +-
 src/include/storage/smgr.h                |  36 ++--
 src/include/utils/rel.h                   |  18 +-
 src/include/utils/relcache.h              |   2 -
 12 files changed, 184 insertions(+), 227 deletions(-)

diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index 43f7b31205d..e7d55dd9b03 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -616,7 +616,11 @@ CreateFakeRelcacheEntry(RelFileLocator rlocator)
 	rel->rd_lockInfo.lockRelId.dbId = rlocator.dbOid;
 	rel->rd_lockInfo.lockRelId.relId = rlocator.relNumber;
 
-	rel->rd_smgr = NULL;
+	/*
+	 * Set up a non-pinned SMgrRelation reference, so that we don't need to
+	 * worry about unpinning it on error.
+	 */
+	rel->rd_smgr = smgropen(rlocator, InvalidBackendId);
 
 	return rel;
 }
@@ -627,9 +631,6 @@ CreateFakeRelcacheEntry(RelFileLocator rlocator)
 void
 FreeFakeRelcacheEntry(Relation fakerel)
 {
-	/* make sure the fakerel is not referenced by the SmgrRelation anymore */
-	if (fakerel->rd_smgr != NULL)
-		smgrclearowner(&fakerel->rd_smgr, fakerel->rd_smgr);
 	pfree(fakerel);
 }
 
@@ -656,10 +657,10 @@ XLogDropDatabase(Oid dbid)
 	/*
 	 * This is unnecessarily heavy-handed, as it will close SMgrRelation
 	 * objects for other databases as well. DROP DATABASE occurs seldom enough
-	 * that it's not worth introducing a variant of smgrclose for just this
-	 * purpose. XXX: Or should we rather leave the smgr entries dangling?
+	 * that it's not worth introducing a variant of smgrdestroy for just this
+	 * purpose.
 	 */
-	smgrcloseall();
+	smgrdestroyall();
 
 	forget_invalid_pages_db(dbid);
 }
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index a3bef6ac34f..7113362eb36 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1422,25 +1422,6 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	heap_freetuple(reltup2);
 
 	table_close(relRelation, RowExclusiveLock);
-
-	/*
-	 * Close both relcache entries' smgr links.  We need this kluge because
-	 * both links will be invalidated during upcoming CommandCounterIncrement.
-	 * Whichever of the rels is the second to be cleared will have a dangling
-	 * reference to the other's smgr entry.  Rather than trying to avoid this
-	 * by ordering operations just so, it's easiest to close the links first.
-	 * (Fortunately, since one of the entries is local in our transaction,
-	 * it's sufficient to clear out our own relcache this way; the problem
-	 * cannot arise for other backends when they see our update on the
-	 * non-transient relation.)
-	 *
-	 * Caution: the placement of this step interacts with the decision to
-	 * handle toast rels by recursion.  When we are trying to rebuild pg_class
-	 * itself, the smgr close on pg_class must happen after all accesses in
-	 * this function.
-	 */
-	RelationCloseSmgrByOid(r1);
-	RelationCloseSmgrByOid(r2);
 }
 
 /*
diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c
index e14c88ac060..ffdcbe2d902 100644
--- a/src/backend/postmaster/bgwriter.c
+++ b/src/backend/postmaster/bgwriter.c
@@ -239,10 +239,12 @@ BackgroundWriterMain(void)
 		if (FirstCallSinceLastCheckpoint())
 		{
 			/*
-			 * After any checkpoint, close all smgr files.  This is so we
-			 * won't hang onto smgr references to deleted files indefinitely.
+			 * After any checkpoint, free all smgr objects.  Otherwise we
+			 * would never do so for dropped relations, as the bgwriter does
+			 * not process shared invalidation messages or call
+			 * AtEOXact_SMgr().
 			 */
-			smgrcloseall();
+			smgrdestroyall();
 		}
 
 		/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 89ab97bdbc7..46c3f034d5d 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -442,10 +442,12 @@ CheckpointerMain(void)
 				ckpt_performed = CreateRestartPoint(flags);
 
 			/*
-			 * After any checkpoint, close all smgr files.  This is so we
-			 * won't hang onto smgr references to deleted files indefinitely.
+			 * After any checkpoint, free all smgr objects.  Otherwise we
+			 * would never do so for dropped relations, as the checkpointer
+			 * does not process shared invalidation messages or call
+			 * AtEOXact_SMgr().
 			 */
-			smgrcloseall();
+			smgrdestroyall();
 
 			/*
 			 * Indicate checkpoint completion to any waiting backends.
@@ -928,11 +930,8 @@ RequestCheckpoint(int flags)
 		 */
 		CreateCheckPoint(flags | CHECKPOINT_IMMEDIATE);
 
-		/*
-		 * After any checkpoint, close all smgr files.  This is so we won't
-		 * hang onto smgr references to deleted files indefinitely.
-		 */
-		smgrcloseall();
+		/* Free all smgr objects, as CheckpointerMain() normally would. */
+		smgrdestroyall();
 
 		return;
 	}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f7c67d504cd..9beeab9d04e 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -934,10 +934,6 @@ ExtendBufferedRelTo(BufferManagerRelation bmr,
 	{
 		LockRelationForExtension(bmr.rel, ExclusiveLock);
 
-		/* could have been closed while waiting for lock */
-		if (bmr.rel)
-			bmr.smgr = RelationGetSmgr(bmr.rel);
-
 		/* recheck, fork might have been created concurrently */
 		if (!smgrexists(bmr.smgr, fork))
 			smgrcreate(bmr.smgr, fork, flags & EB_PERFORMING_RECOVERY);
@@ -1897,11 +1893,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
 	 * we get the lock.
 	 */
 	if (!(flags & EB_SKIP_EXTENSION_LOCK))
-	{
 		LockRelationForExtension(bmr.rel, ExclusiveLock);
-		if (bmr.rel)
-			bmr.smgr = RelationGetSmgr(bmr.rel);
-	}
 
 	/*
 	 * If requested, invalidate size cache, so that smgrnblocks asks the
@@ -4155,6 +4147,7 @@ FlushRelationBuffers(Relation rel)
 {
 	int			i;
 	BufferDesc *bufHdr;
+	SMgrRelation srel = RelationGetSmgr(rel);
 
 	if (RelationUsesLocalBuffers(rel))
 	{
@@ -4183,7 +4176,7 @@ FlushRelationBuffers(Relation rel)
 
 				io_start = pgstat_prepare_io_time();
 
-				smgrwrite(RelationGetSmgr(rel),
+				smgrwrite(srel,
 						  BufTagGetForkNum(&bufHdr->tag),
 						  bufHdr->tag.blockNum,
 						  localpage,
@@ -4229,7 +4222,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+			FlushBuffer(bufHdr, srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -4442,13 +4435,17 @@ void
 CreateAndCopyRelationData(RelFileLocator src_rlocator,
 						  RelFileLocator dst_rlocator, bool permanent)
 {
-	RelFileLocatorBackend rlocator;
 	char		relpersistence;
+	SMgrRelation src_rel;
+	SMgrRelation dst_rel;
 
 	/* Set the relpersistence. */
 	relpersistence = permanent ?
 		RELPERSISTENCE_PERMANENT : RELPERSISTENCE_UNLOGGED;
 
+	src_rel = smgropen(src_rlocator, InvalidBackendId);
+	dst_rel = smgropen(dst_rlocator, InvalidBackendId);
+
 	/*
 	 * Create and copy all forks of the relation.  During create database we
 	 * have a separate cleanup mechanism which deletes complete database
@@ -4465,9 +4462,9 @@ CreateAndCopyRelationData(RelFileLocator src_rlocator,
 	for (ForkNumber forkNum = MAIN_FORKNUM + 1;
 		 forkNum <= MAX_FORKNUM; forkNum++)
 	{
-		if (smgrexists(smgropen(src_rlocator, InvalidBackendId), forkNum))
+		if (smgrexists(src_rel, forkNum))
 		{
-			smgrcreate(smgropen(dst_rlocator, InvalidBackendId), forkNum, false);
+			smgrcreate(dst_rel, forkNum, false);
 
 			/*
 			 * WAL log creation if the relation is persistent, or this is the
@@ -4481,15 +4478,6 @@ CreateAndCopyRelationData(RelFileLocator src_rlocator,
 										   permanent);
 		}
 	}
-
-	/* close source and destination smgr if exists. */
-	rlocator.backend = InvalidBackendId;
-
-	rlocator.locator = src_rlocator;
-	smgrcloserellocator(rlocator);
-
-	rlocator.locator = dst_rlocator;
-	smgrcloserellocator(rlocator);
 }
 
 /* ---------------------------------------------------------------------
diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c
index fb9440ff72f..71b386137f0 100644
--- a/src/backend/storage/freespace/freespace.c
+++ b/src/backend/storage/freespace/freespace.c
@@ -532,14 +532,7 @@ fsm_readbuf(Relation rel, FSMAddress addr, bool extend)
 {
 	BlockNumber blkno = fsm_logical_to_physical(addr);
 	Buffer		buf;
-	SMgrRelation reln;
-
-	/*
-	 * Caution: re-using this smgr pointer could fail if the relcache entry
-	 * gets closed.  It's safe as long as we only do smgr-level operations
-	 * between here and the last use of the pointer.
-	 */
-	reln = RelationGetSmgr(rel);
+	SMgrRelation reln = RelationGetSmgr(rel);
 
 	/*
 	 * If we haven't cached the size of the FSM yet, check it first.  Also
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 4c552649336..419fba0b2b9 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -3,8 +3,42 @@
  * smgr.c
  *	  public interface routines to storage manager switch.
  *
- *	  All file system operations in POSTGRES dispatch through these
- *	  routines.
+ * All file system operations on relations dispatch through these routines.
+ * An SMgrRelation represents physical on-disk relation files that are open
+ * for reading and writing.
+ *
+ * When a relation is first accessed through the relation cache, the
+ * corresponding SMgrRelation entry is opened by calling smgropen(), and the
+ * reference is stored in the relation cache entry.
+ *
+ * Accesses that don't go through the relation cache open the SMgrRelation
+ * directly.  That includes flushing buffers from the buffer cache, as well as
+ * all accesses in auxiliary processes like the checkpointer or the WAL redo
+ * in the startup process.
+ *
+ * Operations like CREATE, DROP, ALTER TABLE also hold SMgrRelation references
+ * independent of the relation cache.  They need to prepare the physical files
+ * before updating the relation cache.
+ *
+ * There is a hash table that holds all the SMgrRelation entries in the
+ * backend.  If you call smgropen() twice for the same rel locator, you get a
+ * reference to the same SMgrRelation. The reference is valid until the end of
+ * transaction.  This makes repeated access to the same relation efficient,
+ * and allows caching things like the relation size in the SMgrRelation entry.
+ *
+ * At end of transaction, all SMgrRelation entries that haven't been pinned
+ * are removed.  An SMgrRelation can hold kernel file system descriptors for
+ * the underlying files, and we'd like to close those reasonably soon if the
+ * file gets deleted.  The SMgrRelations references held by the relcache are
+ * pinned to prevent them from being closed.
+ *
+ * There is another mechanism to close file descriptors early:
+ * PROCSIGNAL_BARRIER_SMGRRELEASE.  It is a request to immediately close all
+ * file descriptors.  Upon receiveing that signal, the backend closes all file
+ * descriptors held open by SMgrRelations, but because it can happen in the
+ * middle of a transaction, we cannot destroy the SMgrRelation objects
+ * themselves, as there could pointers to them in active use.  See
+ * smgrrelease() and smgrreleaseall().
  *
  * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -93,14 +127,15 @@ static const int NSmgr = lengthof(smgrsw);
 
 /*
  * Each backend has a hashtable that stores all extant SMgrRelation objects.
- * In addition, "unowned" SMgrRelation objects are chained together in a list.
+ * In addition, "unpinned" SMgrRelation objects are chained together in a list.
  */
 static HTAB *SMgrRelationHash = NULL;
 
-static dlist_head unowned_relns;
+static dlist_head unpinned_relns;
 
 /* local function prototypes */
 static void smgrshutdown(int code, Datum arg);
+static void smgrdestroy(SMgrRelation reln);
 
 
 /*
@@ -144,7 +179,16 @@ smgrshutdown(int code, Datum arg)
 /*
  * smgropen() -- Return an SMgrRelation object, creating it if need be.
  *
- * This does not attempt to actually open the underlying file.
+ * In versions of PostgreSQL prior to 17, this function returned an object
+ * with no defined lifetime.  Now, however, the object remains valid for the
+ * lifetime of the transaction, up to the point where AtEOXact_SMgr() is
+ * called, making it much easier for callers to know for how long they can
+ * hold on to a pointer to the returned object.  If this function is called
+ * outside of a transaction, the object remains valid until smgrdestroy() or
+ * smgrdestroyall() is called.  Background processes that use smgr but not
+ * transactions typically do this once per checkpoint cycle.
+ *
+ * This does not attempt to actually open the underlying files.
  */
 SMgrRelation
 smgropen(RelFileLocator rlocator, BackendId backend)
@@ -164,7 +208,7 @@ smgropen(RelFileLocator rlocator, BackendId backend)
 		ctl.entrysize = sizeof(SMgrRelationData);
 		SMgrRelationHash = hash_create("smgr relation table", 400,
 									   &ctl, HASH_ELEM | HASH_BLOBS);
-		dlist_init(&unowned_relns);
+		dlist_init(&unpinned_relns);
 	}
 
 	/* Look up or create an entry */
@@ -178,7 +222,6 @@ smgropen(RelFileLocator rlocator, BackendId backend)
 	if (!found)
 	{
 		/* hash_search already filled in the lookup key */
-		reln->smgr_owner = NULL;
 		reln->smgr_targblock = InvalidBlockNumber;
 		for (int i = 0; i <= MAX_FORKNUM; ++i)
 			reln->smgr_cached_nblocks[i] = InvalidBlockNumber;
@@ -187,102 +230,61 @@ smgropen(RelFileLocator rlocator, BackendId backend)
 		/* implementation-specific initialization */
 		smgrsw[reln->smgr_which].smgr_open(reln);
 
-		/* it has no owner yet */
-		dlist_push_tail(&unowned_relns, &reln->node);
+		/* it is not pinned yet */
+		reln->pincount = 0;
+		dlist_push_tail(&unpinned_relns, &reln->node);
 	}
 
 	return reln;
 }
 
 /*
- * smgrsetowner() -- Establish a long-lived reference to an SMgrRelation object
- *
- * There can be only one owner at a time; this is sufficient since currently
- * the only such owners exist in the relcache.
+ * smgrpin() -- Prevent an SMgrRelation object from being destroyed at end of
+ *				of transaction
  */
 void
-smgrsetowner(SMgrRelation *owner, SMgrRelation reln)
+smgrpin(SMgrRelation reln)
 {
-	/* We don't support "disowning" an SMgrRelation here, use smgrclearowner */
-	Assert(owner != NULL);
-
-	/*
-	 * First, unhook any old owner.  (Normally there shouldn't be any, but it
-	 * seems possible that this can happen during swap_relation_files()
-	 * depending on the order of processing.  It's ok to close the old
-	 * relcache entry early in that case.)
-	 *
-	 * If there isn't an old owner, then the reln should be in the unowned
-	 * list, and we need to remove it.
-	 */
-	if (reln->smgr_owner)
-		*(reln->smgr_owner) = NULL;
-	else
+	if (reln->pincount == 0)
 		dlist_delete(&reln->node);
-
-	/* Now establish the ownership relationship. */
-	reln->smgr_owner = owner;
-	*owner = reln;
+	reln->pincount++;
 }
 
 /*
- * smgrclearowner() -- Remove long-lived reference to an SMgrRelation object
- *					   if one exists
+ * smgrunpin() -- Allow an SMgrRelation object to be desroyed at end of
+ *				  transaction
+ *
+ * The object remains valid, but if there are no other pins on it, it is moved
+ * to the unpinned list where it will be destroyed by AtEOXact_SMgr().
  */
 void
-smgrclearowner(SMgrRelation *owner, SMgrRelation reln)
-{
-	/* Do nothing if the SMgrRelation object is not owned by the owner */
-	if (reln->smgr_owner != owner)
-		return;
-
-	/* unset the owner's reference */
-	*owner = NULL;
-
-	/* unset our reference to the owner */
-	reln->smgr_owner = NULL;
-
-	/* add to list of unowned relations */
-	dlist_push_tail(&unowned_relns, &reln->node);
-}
-
-/*
- * smgrexists() -- Does the underlying file for a fork exist?
- */
-bool
-smgrexists(SMgrRelation reln, ForkNumber forknum)
+smgrunpin(SMgrRelation reln)
 {
-	return smgrsw[reln->smgr_which].smgr_exists(reln, forknum);
+	Assert(reln->pincount > 0);
+	reln->pincount--;
+	if (reln->pincount == 0)
+		dlist_push_tail(&unpinned_relns, &reln->node);
 }
 
 /*
- * smgrclose() -- Close and delete an SMgrRelation object.
+ * smgrdestroy() -- Delete an SMgrRelation object.
  */
-void
-smgrclose(SMgrRelation reln)
+static void
+smgrdestroy(SMgrRelation reln)
 {
-	SMgrRelation *owner;
 	ForkNumber	forknum;
 
+	Assert(reln->pincount == 0);
+
 	for (forknum = 0; forknum <= MAX_FORKNUM; forknum++)
 		smgrsw[reln->smgr_which].smgr_close(reln, forknum);
 
-	owner = reln->smgr_owner;
-
-	if (!owner)
-		dlist_delete(&reln->node);
+	dlist_delete(&reln->node);
 
 	if (hash_search(SMgrRelationHash,
 					&(reln->smgr_rlocator),
 					HASH_REMOVE, NULL) == NULL)
 		elog(ERROR, "SMgrRelation hashtable corrupted");
-
-	/*
-	 * Unhook the owner pointer, if any.  We do this last since in the remote
-	 * possibility of failure above, the SMgrRelation object will still exist.
-	 */
-	if (owner)
-		*owner = NULL;
 }
 
 /*
@@ -302,31 +304,51 @@ smgrrelease(SMgrRelation reln)
 }
 
 /*
- * smgrreleaseall() -- Release resources used by all objects.
+ * smgrclose() -- Close an SMgrRelation object.
  *
- * This is called for PROCSIGNAL_BARRIER_SMGRRELEASE.
+ * The SMgrRelation reference should not be used after this call.  However,
+ * because we don't keep track of the references returned by smgropen(), we
+ * don't know if there are other references still pointing to the same object,
+ * so we cannot remove the SMgrRelation object yet.  Therefore, this is just a
+ * synonym for smgrrelease() at the moment.
  */
 void
-smgrreleaseall(void)
+smgrclose(SMgrRelation reln)
 {
-	HASH_SEQ_STATUS status;
-	SMgrRelation reln;
+	smgrrelease(reln);
+}
 
-	/* Nothing to do if hashtable not set up */
-	if (SMgrRelationHash == NULL)
-		return;
+/*
+ * smgrdestroyall() -- Release resources used by all unpinned objects.
+ *
+ * It must be known that there are no pointers to SMgrRelations, other than
+ * those pinned with smgrpin().
+ */
+void
+smgrdestroyall(void)
+{
+	dlist_mutable_iter iter;
 
-	hash_seq_init(&status, SMgrRelationHash);
+	/*
+	 * Zap all unpinned SMgrRelations.  We rely on smgrdestroy() to remove
+	 * each one from the list.
+	 */
+	dlist_foreach_modify(iter, &unpinned_relns)
+	{
+		SMgrRelation rel = dlist_container(SMgrRelationData, node,
+										   iter.cur);
 
-	while ((reln = (SMgrRelation) hash_seq_search(&status)) != NULL)
-		smgrrelease(reln);
+		smgrdestroy(rel);
+	}
 }
 
 /*
- * smgrcloseall() -- Close all existing SMgrRelation objects.
+ * smgrreleaseall() -- Release resources used by all objects.
+ *
+ * This is called for PROCSIGNAL_BARRIER_SMGRRELEASE.
  */
 void
-smgrcloseall(void)
+smgrreleaseall(void)
 {
 	HASH_SEQ_STATUS status;
 	SMgrRelation reln;
@@ -338,19 +360,21 @@ smgrcloseall(void)
 	hash_seq_init(&status, SMgrRelationHash);
 
 	while ((reln = (SMgrRelation) hash_seq_search(&status)) != NULL)
-		smgrclose(reln);
+	{
+		smgrrelease(reln);
+	}
 }
 
 /*
- * smgrcloserellocator() -- Close SMgrRelation object for given RelFileLocator,
- *							if one exists.
+ * smgrreleaserellocator() -- Release resources for given RelFileLocator,
+ *							if it's open.
  *
- * This has the same effects as smgrclose(smgropen(rlocator)), but it avoids
+ * This has the same effects as smgrrelease(smgropen(rlocator)), but it avoids
  * uselessly creating a hashtable entry only to drop it again when no
  * such entry exists already.
  */
 void
-smgrcloserellocator(RelFileLocatorBackend rlocator)
+smgrreleaserellocator(RelFileLocatorBackend rlocator)
 {
 	SMgrRelation reln;
 
@@ -362,7 +386,16 @@ smgrcloserellocator(RelFileLocatorBackend rlocator)
 									  &rlocator,
 									  HASH_FIND, NULL);
 	if (reln != NULL)
-		smgrclose(reln);
+		smgrrelease(reln);
+}
+
+/*
+ * smgrexists() -- Does the underlying file for a fork exist?
+ */
+bool
+smgrexists(SMgrRelation reln, ForkNumber forknum)
+{
+	return smgrsw[reln->smgr_which].smgr_exists(reln, forknum);
 }
 
 /*
@@ -729,7 +762,8 @@ smgrimmedsync(SMgrRelation reln, ForkNumber forknum)
  * AtEOXact_SMgr
  *
  * This routine is called during transaction commit or abort (it doesn't
- * particularly care which).  All transient SMgrRelation objects are closed.
+ * particularly care which).  All transient SMgrRelation objects are
+ * destroyed.
  *
  * We do this as a compromise between wanting transient SMgrRelations to
  * live awhile (to amortize the costs of blind writes of multiple blocks)
@@ -740,21 +774,7 @@ smgrimmedsync(SMgrRelation reln, ForkNumber forknum)
 void
 AtEOXact_SMgr(void)
 {
-	dlist_mutable_iter iter;
-
-	/*
-	 * Zap all unowned SMgrRelations.  We rely on smgrclose() to remove each
-	 * one from the list.
-	 */
-	dlist_foreach_modify(iter, &unowned_relns)
-	{
-		SMgrRelation rel = dlist_container(SMgrRelationData, node,
-										   iter.cur);
-
-		Assert(rel->smgr_owner == NULL);
-
-		smgrclose(rel);
-	}
+	smgrdestroyall();
 }
 
 /*
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index a041d7d6047..edaa56194ff 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -756,7 +756,7 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg)
 
 		rlocator.locator = msg->sm.rlocator;
 		rlocator.backend = (msg->sm.backend_hi << 16) | (int) msg->sm.backend_lo;
-		smgrcloserellocator(rlocator);
+		smgrreleaserellocator(rlocator);
 	}
 	else if (msg->id == SHAREDINVALRELMAP_ID)
 	{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index b3faccbefe5..b4c7dacdc9e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -3044,7 +3044,7 @@ RelationCacheInvalidate(bool debug_discard)
 	 * start to rebuild entries, since that may involve catalog fetches which
 	 * will re-open catalog files.
 	 */
-	smgrcloseall();
+	smgrdestroyall();
 
 	/* Phase 2: rebuild the items found to need rebuild in phase 1 */
 	foreach(l, rebuildFirstList)
@@ -3066,25 +3066,6 @@ RelationCacheInvalidate(bool debug_discard)
 			in_progress_list[i].invalidated = true;
 }
 
-/*
- * RelationCloseSmgrByOid - close a relcache entry's smgr link
- *
- * Needed in some cases where we are changing a relation's physical mapping.
- * The link will be automatically reopened on next use.
- */
-void
-RelationCloseSmgrByOid(Oid relationId)
-{
-	Relation	relation;
-
-	RelationIdCacheLookup(relationId, relation);
-
-	if (!PointerIsValid(relation))
-		return;					/* not in cache, nothing to do */
-
-	RelationCloseSmgr(relation);
-}
-
 static void
 RememberToFreeTupleDescAtEOX(TupleDesc td)
 {
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index a9a179aabac..edccc4c89bb 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -21,29 +21,21 @@
 /*
  * smgr.c maintains a table of SMgrRelation objects, which are essentially
  * cached file handles.  An SMgrRelation is created (if not already present)
- * by smgropen(), and destroyed by smgrclose().  Note that neither of these
- * operations imply I/O, they just create or destroy a hashtable entry.
- * (But smgrclose() may release associated resources, such as OS-level file
+ * by smgropen(), and destroyed by smgrdestroy().  Note that neither of these
+ * operations imply I/O, they just create or destroy a hashtable entry.  (But
+ * smgrdestroy() may release associated resources, such as OS-level file
  * descriptors.)
  *
- * An SMgrRelation may have an "owner", which is just a pointer to it from
- * somewhere else; smgr.c will clear this pointer if the SMgrRelation is
- * closed.  We use this to avoid dangling pointers from relcache to smgr
- * without having to make the smgr explicitly aware of relcache.  There
- * can't be more than one "owner" pointer per SMgrRelation, but that's
- * all we need.
- *
- * SMgrRelations that do not have an "owner" are considered to be transient,
- * and are deleted at end of transaction.
+ * An SMgrRelation may be "pinned", to prevent it from being destroyed while
+ * it's in use.  We use this to prevent pointers relcache to smgr from being
+ * invalidated.  SMgrRelations that are not pinned are deleted at end of
+ * transaction.
  */
 typedef struct SMgrRelationData
 {
 	/* rlocator is the hashtable lookup key, so it must be first! */
 	RelFileLocatorBackend smgr_rlocator;	/* relation physical identifier */
 
-	/* pointer to owning pointer, or NULL if none */
-	struct SMgrRelationData **smgr_owner;
-
 	/*
 	 * The following fields are reset to InvalidBlockNumber upon a cache flush
 	 * event, and hold the last known size for each fork.  This information is
@@ -68,7 +60,11 @@ typedef struct SMgrRelationData
 	int			md_num_open_segs[MAX_FORKNUM + 1];
 	struct _MdfdVec *md_seg_fds[MAX_FORKNUM + 1];
 
-	/* if unowned, list link in list of all unowned SMgrRelations */
+	/*
+	 * Pinning support.  If unpinned (pincount == 0), 'node' is a list link in
+	 * list of all unpinned SMgrRelations.
+	 */
+	int			pincount;
 	dlist_node	node;
 } SMgrRelationData;
 
@@ -80,13 +76,13 @@ typedef SMgrRelationData *SMgrRelation;
 extern void smgrinit(void);
 extern SMgrRelation smgropen(RelFileLocator rlocator, BackendId backend);
 extern bool smgrexists(SMgrRelation reln, ForkNumber forknum);
-extern void smgrsetowner(SMgrRelation *owner, SMgrRelation reln);
-extern void smgrclearowner(SMgrRelation *owner, SMgrRelation reln);
+extern void smgrpin(SMgrRelation reln);
+extern void smgrunpin(SMgrRelation reln);
 extern void smgrclose(SMgrRelation reln);
-extern void smgrcloseall(void);
-extern void smgrcloserellocator(RelFileLocatorBackend rlocator);
+extern void smgrdestroyall(void);
 extern void smgrrelease(SMgrRelation reln);
 extern void smgrreleaseall(void);
+extern void smgrreleaserellocator(RelFileLocatorBackend rlocator);
 extern void smgrcreate(SMgrRelation reln, ForkNumber forknum, bool isRedo);
 extern void smgrdosyncall(SMgrRelation *rels, int nrels);
 extern void smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 0ad613c4b88..5ab7d54d897 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -561,18 +561,15 @@ typedef struct ViewOptions
  *
  * Very little code is authorized to touch rel->rd_smgr directly.  Instead
  * use this function to fetch its value.
- *
- * Note: since a relcache flush can cause the file handle to be closed again,
- * it's unwise to hold onto the pointer returned by this function for any
- * long period.  Recommended practice is to just re-execute RelationGetSmgr
- * each time you need to access the SMgrRelation.  It's quite cheap in
- * comparison to whatever an smgr function is going to do.
  */
 static inline SMgrRelation
 RelationGetSmgr(Relation rel)
 {
 	if (unlikely(rel->rd_smgr == NULL))
-		smgrsetowner(&(rel->rd_smgr), smgropen(rel->rd_locator, rel->rd_backend));
+	{
+		rel->rd_smgr = smgropen(rel->rd_locator, rel->rd_backend);
+		smgrpin(rel->rd_smgr);
+	}
 	return rel->rd_smgr;
 }
 
@@ -584,10 +581,11 @@ static inline void
 RelationCloseSmgr(Relation relation)
 {
 	if (relation->rd_smgr != NULL)
+	{
+		smgrunpin(relation->rd_smgr);
 		smgrclose(relation->rd_smgr);
-
-	/* smgrclose should unhook from owner pointer */
-	Assert(relation->rd_smgr == NULL);
+		relation->rd_smgr = NULL;
+	}
 }
 #endif							/* !FRONTEND */
 
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index f04b2477e34..c1c31baa429 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -129,8 +129,6 @@ extern void RelationCacheInvalidateEntry(Oid relationId);
 
 extern void RelationCacheInvalidate(bool debug_discard);
 
-extern void RelationCloseSmgrByOid(Oid relationId);
-
 #ifdef USE_ASSERT_CHECKING
 extern void AssertPendingSyncs_RelationCache(void);
 #else
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 43+ messages in thread

* Re: Extending SMgrRelation lifetimes
@ 2023-12-08 07:20  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 0 replies; 43+ messages in thread

From: Heikki Linnakangas @ 2023-12-08 07:20 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; Thomas Munro <[email protected]>; +Cc: pgsql-hackers

On 29/11/2023 14:41, Heikki Linnakangas wrote:
> 2. A funny case with foreign tables: ANALYZE on a foreign table calls
> visibilitymap_count(). A foreign table has no visibility map so it
> returns 0, but before doing so it calls RelationGetSmgr on the foreign
> table, which has 0/0/0 rellocator. That creates an SMgrRelation for
> 0/0/0, and sets the foreign table's relcache entry as its owner. If you
> then call ANALYZE on another foreign table, it also calls
> RelationGetSmgr with 0/0/0 rellocator, returning the same SMgrRelation
> entry, and changes its owner to the new relcache entry. That doesn't
> make much sense and it's pretty accidental that it works at all, so
> attached is a patch to avoid calling visibilitymap_count() on foreign
> tables.

This patch seems uncontroversial and independent of the others, so I 
committed it.

-- 
Heikki Linnakangas
Neon (https://neon.tech)






^ permalink  raw  reply  [nested|flat] 43+ messages in thread


end of thread, other threads:[~2023-12-08 07:20 UTC | newest]

Thread overview: 43+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v3 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-08-19 12:34 [PATCH v2 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2020-09-24 16:51 [PATCH v3 4/5] pg_rewind: Refactor the abstraction to fetch from local/libpq source. Heikki Linnakangas <[email protected]>
2023-09-18 15:19 Re: Extending SMgrRelation lifetimes Robert Haas <[email protected]>
2023-11-29 12:41 ` Re: Extending SMgrRelation lifetimes Heikki Linnakangas <[email protected]>
2023-12-08 07:20   ` Re: Extending SMgrRelation lifetimes Heikki Linnakangas <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox