Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Back-patch src/test/recovery and PostgresNode from 9.6 to 9.5.
authorNoah Misch <noah@leadboat.com>
Thu, 19 Mar 2020 16:39:26 +0000 (09:39 -0700)
committerNoah Misch <noah@leadboat.com>
Thu, 19 Mar 2020 16:39:26 +0000 (09:39 -0700)
This omits 007_sync_rep.pl, which tests a feature new in 9.6.  The only
other change is to substitute "hot_standby" for "replica".  A planned
back-patch will use this suite to test its recovery behavior changes.
Identified by Kyotaro Horiguchi, though I did not use his patch.

Discussion: https://postgr.es/m/20200304.162919.898938381201316571.horikyota.ntt@gmail.com

16 files changed:
src/Makefile.global.in
src/test/Makefile
src/test/perl/PostgresNode.pm [new file with mode: 0644]
src/test/perl/RecursiveCopy.pm [new file with mode: 0644]
src/test/perl/TestLib.pm
src/test/recovery/.gitignore [new file with mode: 0644]
src/test/recovery/Makefile [new file with mode: 0644]
src/test/recovery/README [new file with mode: 0644]
src/test/recovery/t/001_stream_rep.pl [new file with mode: 0644]
src/test/recovery/t/002_archiving.pl [new file with mode: 0644]
src/test/recovery/t/003_recovery_targets.pl [new file with mode: 0644]
src/test/recovery/t/004_timeline_switch.pl [new file with mode: 0644]
src/test/recovery/t/005_replay_delay.pl [new file with mode: 0644]
src/test/recovery/t/008_fsm_truncation.pl [new file with mode: 0644]
src/test/recovery/t/017_shm.pl [new file with mode: 0644]
src/tools/msvc/vcregress.pl

index 7caaf7018e13287a62e6b9a8742e463b853cabc2..9f49ad3e5a90b1407799f78e2befd2b281de5aa5 100644 (file)
@@ -371,12 +371,12 @@ ifeq ($(enable_tap_tests),yes)
 
 define prove_installcheck
 rm -rf $(CURDIR)/tmp_check/log
-cd $(srcdir) && TESTDIR='$(CURDIR)' PATH="$(bindir):$$PATH" PGPORT='6$(DEF_PGPORT)' top_builddir='$(CURDIR)/$(top_builddir)' PG_REGRESS='$(CURDIR)/$(top_builddir)/src/test/regress/pg_regress' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) t/*.pl
+cd $(srcdir) && TESTDIR='$(CURDIR)' PATH="$(bindir):$$PATH" PGPORT='6$(DEF_PGPORT)' top_builddir='$(CURDIR)/$(top_builddir)' PG_REGRESS='$(CURDIR)/$(top_builddir)/src/test/regress/pg_regress' REGRESS_SHLIB='$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) t/*.pl
 endef
 
 define prove_check
 rm -rf $(CURDIR)/tmp_check/log
-cd $(srcdir) && TESTDIR='$(CURDIR)' $(with_temp_install) PGPORT='6$(DEF_PGPORT)' PG_REGRESS='$(CURDIR)/$(top_builddir)/src/test/regress/pg_regress' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) t/*.pl
+cd $(srcdir) && TESTDIR='$(CURDIR)' $(with_temp_install) PGPORT='6$(DEF_PGPORT)' PG_REGRESS='$(CURDIR)/$(top_builddir)/src/test/regress/pg_regress' REGRESS_SHLIB='$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) t/*.pl
 endef
 
 else
index 777b2ba94a2f9b929a16f4b7fc1a2a2218ec960f..6b40cf50ed2eb0700d70ec5c3832658852469dab 100644 (file)
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules
+SUBDIRS = perl regress isolation modules recovery
 
 # We don't build or execute examples/, locale/, or thread/ by default,
 # but we do want "make clean" etc to recurse into them.  Likewise for ssl/,
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
new file mode 100644 (file)
index 0000000..3e39fd6
--- /dev/null
@@ -0,0 +1,1481 @@
+
+=pod
+
+=head1 NAME
+
+PostgresNode - class representing PostgreSQL server instance
+
+=head1 SYNOPSIS
+
+  use PostgresNode;
+
+  my $node = PostgresNode->get_new_node('mynode');
+
+  # Create a data directory with initdb
+  $node->init();
+
+  # Start the PostgreSQL server
+  $node->start();
+
+  # Change a setting and restart
+  $node->append_conf('postgresql.conf', 'hot_standby = on');
+  $node->restart();
+
+  # run a query with psql, like:
+  #   echo 'SELECT 1' | psql -qAXt postgres -v ON_ERROR_STOP=1
+  $psql_stdout = $node->safe_psql('postgres', 'SELECT 1');
+
+  # Run psql with a timeout, capturing stdout and stderr
+  # as well as the psql exit code. Pass some extra psql
+  # options. If there's an error from psql raise an exception.
+  my ($stdout, $stderr, $timed_out);
+  my $cmdret = $node->psql('postgres', 'SELECT pg_sleep(60)',
+     stdout => \$stdout, stderr => \$stderr,
+     timeout => 30, timed_out => \$timed_out,
+     extra_params => ['--single-transaction'],
+     on_error_die => 1)
+  print "Sleep timed out" if $timed_out;
+
+  # Similar thing, more convenient in common cases
+  my ($cmdret, $stdout, $stderr) =
+      $node->psql('postgres', 'SELECT 1');
+
+  # run query every second until it returns 't'
+  # or times out
+  $node->poll_query_until('postgres', q|SELECT random() < 0.1;|')
+    or print "timed out";
+
+  # Do an online pg_basebackup
+  my $ret = $node->backup('testbackup1');
+
+  # Take a backup of a running server
+  my $ret = $node->backup_fs_hot('testbackup2');
+
+  # Take a backup of a stopped server
+  $node->stop;
+  my $ret = $node->backup_fs_cold('testbackup3')
+
+  # Restore it to create a new independent node (not a replica)
+  my $replica = get_new_node('replica');
+  $replica->init_from_backup($node, 'testbackup');
+  $replica->start;
+
+  # Stop the server
+  $node->stop('fast');
+
+=head1 DESCRIPTION
+
+PostgresNode contains a set of routines able to work on a PostgreSQL node,
+allowing to start, stop, backup and initialize it with various options.
+The set of nodes managed by a given test is also managed by this module.
+
+In addition to node management, PostgresNode instances have some wrappers
+around Test::More functions to run commands with an environment set up to
+point to the instance.
+
+The IPC::Run module is required.
+
+=cut
+
+package PostgresNode;
+
+use strict;
+use warnings;
+
+use Config;
+use Cwd;
+use Exporter 'import';
+use File::Basename;
+use File::Spec;
+use File::Temp ();
+use IPC::Run;
+use RecursiveCopy;
+use Socket;
+use Test::More;
+use TestLib ();
+use Scalar::Util qw(blessed);
+
+our @EXPORT = qw(
+  get_new_node
+);
+
+our ($use_tcp, $test_localhost, $test_pghost, $last_host_assigned,
+   $last_port_assigned, @all_nodes);
+
+# For backward compatibility only.
+our $vfs_path = '';
+if ($Config{osname} eq 'msys')
+{
+   $vfs_path = `cd / && pwd -W`;
+   chomp $vfs_path;
+}
+
+INIT
+{
+
+   # Set PGHOST for backward compatibility.  This doesn't work for own_host
+   # nodes, so prefer to not rely on this when writing new tests.
+   $use_tcp            = $TestLib::windows_os;
+   $test_localhost     = "127.0.0.1";
+   $last_host_assigned = 1;
+   $test_pghost        = $use_tcp ? $test_localhost : TestLib::tempdir_short;
+   $ENV{PGHOST}        = $test_pghost;
+   $ENV{PGDATABASE}    = 'postgres';
+
+   # Tracking of last port value assigned to accelerate free port lookup.
+   $last_port_assigned = int(rand() * 16384) + 49152;
+}
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item PostgresNode::new($class, $name, $pghost, $pgport)
+
+Create a new PostgresNode instance. Does not initdb or start it.
+
+You should generally prefer to use get_new_node() instead since it takes care
+of finding port numbers, registering instances for cleanup, etc.
+
+=cut
+
+sub new
+{
+   my ($class, $name, $pghost, $pgport) = @_;
+   my $testname = basename($0);
+   $testname =~ s/\.[^.]+$//;
+   my $self = {
+       _port    => $pgport,
+       _host    => $pghost,
+       _basedir => TestLib::tempdir("data_" . $name),
+       _name    => $name,
+       _logfile_generation => 0,
+       _logfile_base       => "$TestLib::log_path/${testname}_${name}",
+       _logfile            => "$TestLib::log_path/${testname}_${name}.log"
+   };
+
+   bless $self, $class;
+   $self->dump_info;
+
+   return $self;
+}
+
+=pod
+
+=item $node->port()
+
+Get the port number assigned to the host. This won't necessarily be a TCP port
+open on the local host since we prefer to use unix sockets if possible.
+
+Use $node->connstr() if you want a connection string.
+
+=cut
+
+sub port
+{
+   my ($self) = @_;
+   return $self->{_port};
+}
+
+=pod
+
+=item $node->host()
+
+Return the host (like PGHOST) for this instance. May be a UNIX socket path.
+
+Use $node->connstr() if you want a connection string.
+
+=cut
+
+sub host
+{
+   my ($self) = @_;
+   return $self->{_host};
+}
+
+=pod
+
+=item $node->basedir()
+
+The directory all the node's files will be within - datadir, archive directory,
+backups, etc.
+
+=cut
+
+sub basedir
+{
+   my ($self) = @_;
+   return $self->{_basedir};
+}
+
+=pod
+
+=item $node->name()
+
+The name assigned to the node at creation time.
+
+=cut
+
+sub name
+{
+   my ($self) = @_;
+   return $self->{_name};
+}
+
+=pod
+
+=item $node->logfile()
+
+Path to the PostgreSQL log file for this instance.
+
+=cut
+
+sub logfile
+{
+   my ($self) = @_;
+   return $self->{_logfile};
+}
+
+=pod
+
+=item $node->connstr()
+
+Get a libpq connection string that will establish a connection to
+this node. Suitable for passing to psql, DBD::Pg, etc.
+
+=cut
+
+sub connstr
+{
+   my ($self, $dbname) = @_;
+   my $pgport = $self->port;
+   my $pghost = $self->host;
+   if (!defined($dbname))
+   {
+       return "port=$pgport host=$pghost";
+   }
+   return "port=$pgport host=$pghost dbname=$dbname";
+}
+
+=pod
+
+=item $node->data_dir()
+
+Returns the path to the data directory. postgresql.conf and pg_hba.conf are
+always here.
+
+=cut
+
+sub data_dir
+{
+   my ($self) = @_;
+   my $res = $self->basedir;
+   return "$res/pgdata";
+}
+
+=pod
+
+=item $node->archive_dir()
+
+If archiving is enabled, WAL files go here.
+
+=cut
+
+sub archive_dir
+{
+   my ($self) = @_;
+   my $basedir = $self->basedir;
+   return "$basedir/archives";
+}
+
+=pod
+
+=item $node->backup_dir()
+
+The output path for backups taken with $node->backup()
+
+=cut
+
+sub backup_dir
+{
+   my ($self) = @_;
+   my $basedir = $self->basedir;
+   return "$basedir/backup";
+}
+
+=pod
+
+=item $node->info()
+
+Return a string containing human-readable diagnostic information (paths, etc)
+about this node.
+
+=cut
+
+sub info
+{
+   my ($self) = @_;
+   my $_info = '';
+   open my $fh, '>', \$_info or die;
+   print $fh "Name: " . $self->name . "\n";
+   print $fh "Data directory: " . $self->data_dir . "\n";
+   print $fh "Backup directory: " . $self->backup_dir . "\n";
+   print $fh "Archive directory: " . $self->archive_dir . "\n";
+   print $fh "Connection string: " . $self->connstr . "\n";
+   print $fh "Log file: " . $self->logfile . "\n";
+   close $fh or die;
+   return $_info;
+}
+
+=pod
+
+=item $node->dump_info()
+
+Print $node->info()
+
+=cut
+
+sub dump_info
+{
+   my ($self) = @_;
+   print $self->info;
+}
+
+
+# Internal method to set up trusted pg_hba.conf for replication.  Not
+# documented because you shouldn't use it, it's called automatically if needed.
+sub set_replication_conf
+{
+   my ($self) = @_;
+   my $pgdata = $self->data_dir;
+
+   $self->host eq $test_pghost
+     or die "set_replication_conf only works with the default host";
+
+   open my $hba, ">>$pgdata/pg_hba.conf";
+   print $hba "\n# Allow replication (set up by PostgresNode.pm)\n";
+   if (!$TestLib::windows_os)
+   {
+       print $hba "local replication all trust\n";
+   }
+   else
+   {
+       print $hba
+"host replication all $test_localhost/32 sspi include_realm=1 map=regress\n";
+   }
+   close $hba;
+}
+
+=pod
+
+=item $node->init(...)
+
+Initialize a new cluster for testing.
+
+Authentication is set up so that only the current OS user can access the
+cluster. On Unix, we use Unix domain socket connections, with the socket in
+a directory that's only accessible to the current user to ensure that.
+On Windows, we use SSPI authentication to ensure the same (by pg_regress
+--config-auth).
+
+pg_hba.conf is configured to allow replication connections. Pass the keyword
+parameter hba_permit_replication => 0 to disable this.
+
+WAL archiving can be enabled on this node by passing the keyword parameter
+has_archiving => 1. This is disabled by default.
+
+postgresql.conf can be set up for replication by passing the keyword
+parameter allows_streaming => 1. This is disabled by default.
+
+The new node is set up in a fast but unsafe configuration where fsync is
+disabled.
+
+=cut
+
+sub init
+{
+   my ($self, %params) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   my $host   = $self->host;
+
+   $params{hba_permit_replication} = 1
+     unless defined $params{hba_permit_replication};
+   $params{allows_streaming} = 0 unless defined $params{allows_streaming};
+   $params{has_archiving}    = 0 unless defined $params{has_archiving};
+
+   mkdir $self->backup_dir;
+   mkdir $self->archive_dir;
+
+   TestLib::system_or_bail('initdb', '-D', $pgdata, '-A', 'trust', '-N');
+   TestLib::system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata);
+
+   open my $conf, ">>$pgdata/postgresql.conf";
+   print $conf "\n# Added by PostgresNode.pm\n";
+   print $conf "fsync = off\n";
+   print $conf "restart_after_crash = off\n";
+   print $conf "log_statement = all\n";
+   print $conf "wal_retrieve_retry_interval = '500ms'\n";
+
+   # If a setting tends to affect whether tests pass or fail, print it after
+   # TEMP_CONFIG.  Otherwise, print it before TEMP_CONFIG, thereby permitting
+   # overrides.  Settings that merely improve performance or ease debugging
+   # belong before TEMP_CONFIG.
+   print $conf TestLib::slurp_file($ENV{TEMP_CONFIG})
+     if defined $ENV{TEMP_CONFIG};
+
+   # XXX Neutralize any stats_temp_directory in TEMP_CONFIG.  Nodes running
+   # concurrently must not share a stats_temp_directory.
+   print $conf "stats_temp_directory = 'pg_stat_tmp'\n";
+
+   if ($params{allows_streaming})
+   {
+       print $conf "wal_level = hot_standby\n";
+       print $conf "max_wal_senders = 5\n";
+       print $conf "wal_keep_segments = 20\n";
+       print $conf "max_wal_size = 128MB\n";
+       print $conf "shared_buffers = 1MB\n";
+       print $conf "wal_log_hints = on\n";
+       print $conf "hot_standby = on\n";
+       print $conf "max_connections = 10\n";
+   }
+
+   print $conf "port = $port\n";
+   if ($use_tcp)
+   {
+       print $conf "unix_socket_directories = ''\n";
+       print $conf "listen_addresses = '$host'\n";
+   }
+   else
+   {
+       print $conf "unix_socket_directories = '$host'\n";
+       print $conf "listen_addresses = ''\n";
+   }
+   close $conf;
+
+   $self->set_replication_conf if $params{hba_permit_replication};
+   $self->enable_archiving     if $params{has_archiving};
+}
+
+=pod
+
+=item $node->append_conf(filename, str)
+
+A shortcut method to append to files like pg_hba.conf and postgresql.conf.
+
+Does no validation or sanity checking. Does not reload the configuration
+after writing.
+
+A newline is automatically appended to the string.
+
+=cut
+
+sub append_conf
+{
+   my ($self, $filename, $str) = @_;
+
+   my $conffile = $self->data_dir . '/' . $filename;
+
+   TestLib::append_to_file($conffile, $str . "\n");
+}
+
+=pod
+
+=item $node->backup(backup_name)
+
+Create a hot backup with B<pg_basebackup> in subdirectory B<backup_name> of
+B<< $node->backup_dir >>, including the transaction logs. Transaction logs are
+fetched at the end of the backup, not streamed.
+
+You'll have to configure a suitable B<max_wal_senders> on the
+target server since it isn't done by default.
+
+=cut
+
+sub backup
+{
+   my ($self, $backup_name) = @_;
+   my $backup_path = $self->backup_dir . '/' . $backup_name;
+   my $name        = $self->name;
+
+   print "# Taking pg_basebackup $backup_name from node \"$name\"\n";
+   TestLib::system_or_bail('pg_basebackup', '-D', $backup_path, '-h',
+       $self->host, '-p', $self->port, '-x');
+   print "# Backup finished\n";
+}
+
+=item $node->backup_fs_hot(backup_name)
+
+Create a backup with a filesystem level copy in subdirectory B<backup_name> of
+B<< $node->backup_dir >>, including transaction logs.
+
+Archiving must be enabled, as B<pg_start_backup()> and B<pg_stop_backup()> are
+used. This is not checked or enforced.
+
+The backup name is passed as the backup label to B<pg_start_backup()>.
+
+=cut
+
+sub backup_fs_hot
+{
+   my ($self, $backup_name) = @_;
+   $self->_backup_fs($backup_name, 1);
+}
+
+=item $node->backup_fs_cold(backup_name)
+
+Create a backup with a filesystem level copy in subdirectory B<backup_name> of
+B<< $node->backup_dir >>, including transaction logs. The server must be
+stopped as no attempt to handle concurrent writes is made.
+
+Use B<backup> or B<backup_fs_hot> if you want to back up a running server.
+
+=cut
+
+sub backup_fs_cold
+{
+   my ($self, $backup_name) = @_;
+   $self->_backup_fs($backup_name, 0);
+}
+
+
+# Common sub of backup_fs_hot and backup_fs_cold
+sub _backup_fs
+{
+   my ($self, $backup_name, $hot) = @_;
+   my $backup_path = $self->backup_dir . '/' . $backup_name;
+   my $port        = $self->port;
+   my $name        = $self->name;
+
+   print "# Taking filesystem backup $backup_name from node \"$name\"\n";
+
+   if ($hot)
+   {
+       my $stdout = $self->safe_psql('postgres',
+           "SELECT * FROM pg_start_backup('$backup_name');");
+       print "# pg_start_backup: $stdout\n";
+   }
+
+   RecursiveCopy::copypath(
+       $self->data_dir,
+       $backup_path,
+       filterfn => sub {
+           my $src = shift;
+           return ($src ne 'pg_log' and $src ne 'postmaster.pid');
+       });
+
+   if ($hot)
+   {
+
+       # We ignore pg_stop_backup's return value. We also assume archiving
+       # is enabled; otherwise the caller will have to copy the remaining
+       # segments.
+       my $stdout =
+         $self->safe_psql('postgres', 'SELECT * FROM pg_stop_backup();');
+       print "# pg_stop_backup: $stdout\n";
+   }
+
+   print "# Backup finished\n";
+}
+
+
+
+=pod
+
+=item $node->init_from_backup(root_node, backup_name)
+
+Initialize a node from a backup, which may come from this node or a different
+node. root_node must be a PostgresNode reference, backup_name the string name
+of a backup previously created on that node with $node->backup.
+
+Does not start the node after initializing it.
+
+A recovery.conf is not created.
+
+pg_hba.conf is configured to allow replication connections. Pass the keyword
+parameter hba_permit_replication => 0 to disable this.
+
+Streaming replication can be enabled on this node by passing the keyword
+parameter has_streaming => 1. This is disabled by default.
+
+Restoring WAL segments from archives using restore_command can be enabled
+by passing the keyword parameter has_restoring => 1. This is disabled by
+default.
+
+The backup is copied, leaving the original unmodified. pg_hba.conf is
+unconditionally set to enable replication connections.
+
+=cut
+
+sub init_from_backup
+{
+   my ($self, $root_node, $backup_name, %params) = @_;
+   my $backup_path = $root_node->backup_dir . '/' . $backup_name;
+   my $host        = $self->host;
+   my $port        = $self->port;
+   my $node_name   = $self->name;
+   my $root_name   = $root_node->name;
+
+   $params{has_streaming} = 0 unless defined $params{has_streaming};
+   $params{hba_permit_replication} = 1
+     unless defined $params{hba_permit_replication};
+   $params{has_restoring} = 0 unless defined $params{has_restoring};
+
+   print
+"# Initializing node \"$node_name\" from backup \"$backup_name\" of node \"$root_name\"\n";
+   die "Backup \"$backup_name\" does not exist at $backup_path"
+     unless -d $backup_path;
+
+   mkdir $self->backup_dir;
+   mkdir $self->archive_dir;
+
+   my $data_path = $self->data_dir;
+   rmdir($data_path);
+   RecursiveCopy::copypath($backup_path, $data_path);
+   chmod(0700, $data_path);
+
+   # Base configuration for this node
+   $self->append_conf(
+       'postgresql.conf',
+       qq(
+port = $port
+));
+   if ($use_tcp)
+   {
+       $self->append_conf('postgresql.conf', "listen_addresses = '$host'");
+   }
+   else
+   {
+       $self->append_conf('postgresql.conf',
+           "unix_socket_directories = '$host'");
+   }
+   $self->set_replication_conf         if $params{hba_permit_replication};
+   $self->enable_streaming($root_node) if $params{has_streaming};
+   $self->enable_restoring($root_node) if $params{has_restoring};
+}
+
+=pod
+
+=item $node->rotate_logfile()
+
+Switch to a new PostgreSQL log file.  This does not alter any running
+PostgreSQL process.  Subsequent method calls, including pg_ctl invocations,
+will use the new name.  Return the new name.
+
+=cut
+
+sub rotate_logfile
+{
+   my ($self) = @_;
+   $self->{_logfile} = sprintf('%s_%d.log',
+       $self->{_logfile_base},
+       ++$self->{_logfile_generation});
+   return $self->{_logfile};
+}
+
+=pod
+
+=item $node->start(%params) => success_or_failure
+
+Wrapper for pg_ctl -w start
+
+Start the node and wait until it is ready to accept connections.
+
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F<prove> invocation.  If given,
+instead return a true or false value to indicate success or failure.
+
+=back
+
+=cut
+
+sub start
+{
+   my ($self, %params) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   my $name   = $self->name;
+   BAIL_OUT("node \"$name\" is already running") if defined $self->{_pid};
+   print("### Starting node \"$name\"\n");
+   my $ret = TestLib::system_log('pg_ctl', '-w', '-D', $self->data_dir, '-l',
+       $self->logfile, 'start');
+
+   if ($ret != 0)
+   {
+       print "# pg_ctl start failed; logfile:\n";
+       print TestLib::slurp_file($self->logfile);
+       BAIL_OUT("pg_ctl start failed") unless $params{fail_ok};
+       return 0;
+   }
+
+   $self->_update_pid(1);
+   return 1;
+}
+
+=pod
+
+=item $node->kill9()
+
+Send SIGKILL (signal 9) to the postmaster.
+
+Note: if the node is already known stopped, this does nothing.
+However, if we think it's running and it's not, it's important for
+this to fail.  Otherwise, tests might fail to detect server crashes.
+
+=cut
+
+sub kill9
+{
+   my ($self) = @_;
+   my $name = $self->name;
+   return unless defined $self->{_pid};
+   print "### Killing node \"$name\" using signal 9\n";
+   kill(9, $self->{_pid}) or BAIL_OUT("kill(9, $self->{_pid}) failed");
+   $self->{_pid} = undef;
+   return;
+}
+
+=pod
+
+=item $node->stop(mode)
+
+Stop the node using pg_ctl -m $mode and wait for it to stop.
+
+Note: if the node is already known stopped, this does nothing.
+However, if we think it's running and it's not, it's important for
+this to fail.  Otherwise, tests might fail to detect server crashes.
+
+=cut
+
+sub stop
+{
+   my ($self, $mode) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   my $name   = $self->name;
+   $mode = 'fast' unless defined $mode;
+   return unless defined $self->{_pid};
+   print "### Stopping node \"$name\" using mode $mode\n";
+   TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-m', $mode, 'stop');
+   $self->_update_pid(0);
+}
+
+=pod
+
+=item $node->reload()
+
+Reload configuration parameters on the node.
+
+=cut
+
+sub reload
+{
+   my ($self) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   my $name   = $self->name;
+   print "### Reloading node \"$name\"\n";
+   TestLib::system_or_bail('pg_ctl', '-D', $pgdata, 'reload');
+}
+
+=pod
+
+=item $node->restart()
+
+Wrapper for pg_ctl -w restart
+
+=cut
+
+sub restart
+{
+   my ($self)  = @_;
+   my $port    = $self->port;
+   my $pgdata  = $self->data_dir;
+   my $logfile = $self->logfile;
+   my $name    = $self->name;
+   print "### Restarting node \"$name\"\n";
+   TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-w', '-l', $logfile,
+                           'restart');
+   $self->_update_pid(1);
+}
+
+=pod
+
+=item $node->promote()
+
+Wrapper for pg_ctl promote
+
+=cut
+
+sub promote
+{
+   my ($self)  = @_;
+   my $port    = $self->port;
+   my $pgdata  = $self->data_dir;
+   my $logfile = $self->logfile;
+   my $name    = $self->name;
+   print "### Promoting node \"$name\"\n";
+   TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-l', $logfile,
+                           'promote');
+}
+
+# Internal routine to enable streaming replication on a standby node.
+sub enable_streaming
+{
+   my ($self, $root_node) = @_;
+   my $root_connstr = $root_node->connstr;
+   my $name         = $self->name;
+
+   print "### Enabling streaming replication for node \"$name\"\n";
+   $self->append_conf(
+       'recovery.conf', qq(
+primary_conninfo='$root_connstr application_name=$name'
+standby_mode=on
+));
+}
+
+# Internal routine to enable archive recovery command on a standby node
+sub enable_restoring
+{
+   my ($self, $root_node) = @_;
+   my $path = TestLib::perl2host($root_node->archive_dir);
+   my $name = $self->name;
+
+   print "### Enabling WAL restore for node \"$name\"\n";
+
+   # On Windows, the path specified in the restore command needs to use
+   # double back-slashes to work properly and to be able to detect properly
+   # the file targeted by the copy command, so the directory value used
+   # in this routine, using only one back-slash, need to be properly changed
+   # first. Paths also need to be double-quoted to prevent failures where
+   # the path contains spaces.
+   $path =~ s{\\}{\\\\}g if ($TestLib::windows_os);
+   my $copy_command =
+     $TestLib::windows_os
+     ? qq{copy "$path\\\\%f" "%p"}
+     : qq{cp "$path/%f" "%p"};
+
+   $self->append_conf(
+       'recovery.conf', qq(
+restore_command = '$copy_command'
+standby_mode = on
+));
+}
+
+# Internal routine to enable archiving
+sub enable_archiving
+{
+   my ($self) = @_;
+   my $path   = TestLib::perl2host($self->archive_dir);
+   my $name   = $self->name;
+
+   print "### Enabling WAL archiving for node \"$name\"\n";
+
+   # On Windows, the path specified in the restore command needs to use
+   # double back-slashes to work properly and to be able to detect properly
+   # the file targeted by the copy command, so the directory value used
+   # in this routine, using only one back-slash, need to be properly changed
+   # first. Paths also need to be double-quoted to prevent failures where
+   # the path contains spaces.
+   $path =~ s{\\}{\\\\}g if ($TestLib::windows_os);
+   my $copy_command =
+     $TestLib::windows_os
+     ? qq{copy "%p" "$path\\\\%f"}
+     : qq{cp "%p" "$path/%f"};
+
+   # Enable archive_mode and archive_command on node
+   $self->append_conf(
+       'postgresql.conf', qq(
+archive_mode = on
+archive_command = '$copy_command'
+));
+}
+
+# Internal method
+sub _update_pid
+{
+   my ($self, $is_running) = @_;
+   my $name = $self->name;
+
+   # If we can open the PID file, read its first line and that's the PID we
+   # want.
+   if (open my $pidfile, '<', $self->data_dir . "/postmaster.pid")
+   {
+       chomp($self->{_pid} = <$pidfile>);
+       print "# Postmaster PID for node \"$name\" is $self->{_pid}\n";
+       close $pidfile;
+
+       # If we found a pidfile when there shouldn't be one, complain.
+       BAIL_OUT("postmaster.pid unexpectedly present") unless $is_running;
+       return;
+   }
+
+   $self->{_pid} = undef;
+   print "# No postmaster PID for node \"$name\"\n";
+   # Complain if we expected to find a pidfile.
+   BAIL_OUT("postmaster.pid unexpectedly not present") if $is_running;
+}
+
+=pod
+
+=item PostgresNode->get_new_node(node_name, %params)
+
+Build a new object of class C<PostgresNode> (or of a subclass, if you have
+one), assigning a free port number.  Remembers the node, to prevent its port
+number from being reused for another node, and to ensure that it gets
+shut down when the test script exits.
+
+You should generally use this instead of C<PostgresNode::new(...)>.
+
+=over
+
+=item port => [1,65535]
+
+By default, this function assigns a port number to each node.  Specify this to
+force a particular port number.  The caller is responsible for evaluating
+potential conflicts and privilege requirements.
+
+=item own_host => 1
+
+By default, all nodes use the same PGHOST value.  If specified, generate a
+PGHOST specific to this node.  This allows multiple nodes to use the same
+port.
+
+=back
+
+For backwards compatibility, it is also exported as a standalone function,
+which can only create objects of class C<PostgresNode>.
+
+=cut
+
+sub get_new_node
+{
+   my $class = 'PostgresNode';
+   $class = shift if scalar(@_) % 2 != 1;
+   my ($name, %params) = @_;
+   my $port_is_forced = defined $params{port};
+   my $found          = $port_is_forced;
+   my $port = $port_is_forced ? $params{port} : $last_port_assigned;
+
+   while ($found == 0)
+   {
+
+       # advance $port, wrapping correctly around range end
+       $port = 49152 if ++$port >= 65536;
+       print "# Checking port $port\n";
+
+       # Check first that candidate port number is not included in
+       # the list of already-registered nodes.
+       $found = 1;
+       foreach my $node (@all_nodes)
+       {
+           $found = 0 if ($node->port == $port);
+       }
+
+       # Check to see if anything else is listening on this TCP port.  This
+       # is *necessary* on $use_tcp (Windows) configurations.  Seek a port
+       # available for all possible listen_addresses values, for own_host
+       # nodes and so the caller can harness this port for the widest range
+       # of purposes.  The 0.0.0.0 test achieves that for post-2006 Cygwin,
+       # which automatically sets SO_EXCLUSIVEADDRUSE.  The same holds for
+       # MSYS (a Cygwin fork).  Testing 0.0.0.0 is insufficient for Windows
+       # native Perl (https://stackoverflow.com/a/14388707), so we also test
+       # individual addresses.
+       #
+       # This seems like a good idea on Unixen as well, even though we don't
+       # ask the postmaster to open a TCP port on Unix.  On Non-Linux,
+       # non-Windows kernels, binding to 127.0.0.1/24 addresses other than
+       # 127.0.0.1 might fail with EADDRNOTAVAIL.  Binding to 0.0.0.0 is
+       # unnecessary on non-Windows systems.
+       #
+       # XXX A port available now may become unavailable by the time we start
+       # the postmaster.
+       if ($found == 1)
+       {
+           foreach my $addr (qw(127.0.0.1),
+               $use_tcp ? qw(127.0.0.2 127.0.0.3 0.0.0.0) : ())
+           {
+               can_bind($addr, $port) or $found = 0;
+           }
+       }
+   }
+
+   print "# Found port $port\n";
+
+   # Select a host.
+   my $host = $test_pghost;
+   if ($params{own_host})
+   {
+       if ($use_tcp)
+       {
+           $last_host_assigned++;
+           $last_host_assigned > 254 and BAIL_OUT("too many own_host nodes");
+           $host = '127.0.0.' . $last_host_assigned;
+       }
+       else
+       {
+           $host = "$test_pghost/$name"; # Assume $name =~ /^[-_a-zA-Z0-9]+$/
+           mkdir $host;
+       }
+   }
+
+   # Lock port number found by creating a new node
+   my $node = $class->new($name, $host, $port);
+
+   # Add node to list of nodes
+   push(@all_nodes, $node);
+
+   # And update port for next time
+   $port_is_forced or $last_port_assigned = $port;
+
+   return $node;
+}
+
+# Internal routine to check whether a host:port is available to bind
+sub can_bind
+{
+   my ($host, $port) = @_;
+   my $iaddr = inet_aton($host);
+   my $paddr = sockaddr_in($port, $iaddr);
+   my $proto = getprotobyname("tcp");
+
+   socket(SOCK, PF_INET, SOCK_STREAM, $proto)
+     or die "socket failed: $!";
+
+   # As in postmaster, don't use SO_REUSEADDR on Windows
+   setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))
+     unless $TestLib::windows_os;
+   my $ret = bind(SOCK, $paddr) && listen(SOCK, SOMAXCONN);
+   close(SOCK);
+   return $ret;
+}
+
+# Automatically shut down any still-running nodes when the test script exits.
+# Note that this just stops the postmasters (in the same order the nodes were
+# created in).  Temporary PGDATA directories are deleted, in an unspecified
+# order, later when the File::Temp objects are destroyed.
+END
+{
+
+   # take care not to change the script's exit value
+   my $exit_code = $?;
+
+   foreach my $node (@all_nodes)
+   {
+       $node->teardown_node;
+   }
+
+   $? = $exit_code;
+}
+
+=pod
+
+=item $node->teardown_node()
+
+Do an immediate stop of the node
+
+=cut
+
+sub teardown_node
+{
+   my $self = shift;
+
+   $self->stop('immediate');
+}
+
+=pod
+
+=item $node->safe_psql($dbname, $sql) => stdout
+
+Invoke B<psql> to run B<sql> on B<dbname> and return its stdout on success.
+Die if the SQL produces an error. Runs with B<ON_ERROR_STOP> set.
+
+Takes optional extra params like timeout and timed_out parameters with the same
+options as psql.
+
+=cut
+
+sub safe_psql
+{
+   my ($self, $dbname, $sql, %params) = @_;
+
+   my ($stdout, $stderr);
+
+   my $ret = $self->psql(
+       $dbname, $sql,
+       %params,
+       stdout        => \$stdout,
+       stderr        => \$stderr,
+       on_error_die  => 1,
+       on_error_stop => 1);
+
+   # psql can emit stderr from NOTICEs etc
+   if ($stderr ne "")
+   {
+       print "#### Begin standard error\n";
+       print $stderr;
+       print "\n#### End standard error\n";
+   }
+
+   $stdout =~ s/\r//g if $TestLib::windows_os;
+   return $stdout;
+}
+
+=pod
+
+=item $node->psql($dbname, $sql, %params) => psql_retval
+
+Invoke B<psql> to execute B<$sql> on B<$dbname> and return the return value
+from B<psql>, which is run with on_error_stop by default so that it will
+stop running sql and return 3 if the passed SQL results in an error.
+
+As a convenience, if B<psql> is called in array context it returns an
+array containing ($retval, $stdout, $stderr).
+
+psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
+disabled.  That may be overridden by passing extra psql parameters.
+
+stdout and stderr are transformed to UNIX line endings if on Windows. Any
+trailing newline is removed.
+
+Dies on failure to invoke psql but not if psql exits with a nonzero
+return code (unless on_error_die specified).
+
+If psql exits because of a signal, an exception is raised.
+
+=over
+
+=item stdout => \$stdout
+
+B<stdout>, if given, must be a scalar reference to which standard output is
+written.  If not given, standard output is not redirected and will be printed
+unless B<psql> is called in array context, in which case it's captured and
+returned.
+
+=item stderr => \$stderr
+
+Same as B<stdout> but gets standard error. If the same scalar is passed for
+both B<stdout> and B<stderr> the results may be interleaved unpredictably.
+
+=item on_error_stop => 1
+
+By default, the B<psql> method invokes the B<psql> program with ON_ERROR_STOP=1
+set, so SQL execution is stopped at the first error and exit code 2 is
+returned.  Set B<on_error_stop> to 0 to ignore errors instead.
+
+=item on_error_die => 0
+
+By default, this method returns psql's result code. Pass on_error_die to
+instead die with an informative message.
+
+=item timeout => 'interval'
+
+Set a timeout for the psql call as an interval accepted by B<IPC::Run::timer>
+(integer seconds is fine).  This method raises an exception on timeout, unless
+the B<timed_out> parameter is also given.
+
+=item timed_out => \$timed_out
+
+If B<timeout> is set and this parameter is given, the scalar it references
+is set to true if the psql call times out.
+
+=item extra_params => ['--single-transaction']
+
+If given, it must be an array reference containing additional parameters to B<psql>.
+
+=back
+
+e.g.
+
+   my ($stdout, $stderr, $timed_out);
+   my $cmdret = $node->psql('postgres', 'SELECT pg_sleep(60)',
+       stdout => \$stdout, stderr => \$stderr,
+       timeout => 30, timed_out => \$timed_out,
+       extra_params => ['--single-transaction'])
+
+will set $cmdret to undef and $timed_out to a true value.
+
+   $node->psql('postgres', $sql, on_error_die => 1);
+
+dies with an informative message if $sql fails.
+
+=cut
+
+sub psql
+{
+   my ($self, $dbname, $sql, %params) = @_;
+
+   my $stdout            = $params{stdout};
+   my $stderr            = $params{stderr};
+   my $timeout           = undef;
+   my $timeout_exception = 'psql timed out';
+   my @psql_params =
+     ('psql', '-XAtq', '-d', $self->connstr($dbname), '-f', '-');
+
+   # If the caller wants an array and hasn't passed stdout/stderr
+   # references, allocate temporary ones to capture them so we
+   # can return them. Otherwise we won't redirect them at all.
+   if (wantarray)
+   {
+       if (!defined($stdout))
+       {
+           my $temp_stdout = "";
+           $stdout = \$temp_stdout;
+       }
+       if (!defined($stderr))
+       {
+           my $temp_stderr = "";
+           $stderr = \$temp_stderr;
+       }
+   }
+
+   $params{on_error_stop} = 1 unless defined $params{on_error_stop};
+   $params{on_error_die}  = 0 unless defined $params{on_error_die};
+
+   push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
+   push @psql_params, @{ $params{extra_params} }
+     if defined $params{extra_params};
+
+   $timeout =
+     IPC::Run::timeout($params{timeout}, exception => $timeout_exception)
+     if (defined($params{timeout}));
+
+   ${ $params{timed_out} } = 0 if defined $params{timed_out};
+
+   # IPC::Run would otherwise append to existing contents:
+   $$stdout = "" if ref($stdout);
+   $$stderr = "" if ref($stderr);
+
+   my $ret;
+
+   # Run psql and capture any possible exceptions.  If the exception is
+   # because of a timeout and the caller requested to handle that, just return
+   # and set the flag.  Otherwise, and for any other exception, rethrow.
+   #
+   # For background, see
+   # http://search.cpan.org/~ether/Try-Tiny-0.24/lib/Try/Tiny.pm
+   do
+   {
+       local $@;
+       eval {
+           my @ipcrun_opts = (\@psql_params, '<', \$sql);
+           push @ipcrun_opts, '>',  $stdout if defined $stdout;
+           push @ipcrun_opts, '2>', $stderr if defined $stderr;
+           push @ipcrun_opts, $timeout if defined $timeout;
+
+           IPC::Run::run @ipcrun_opts;
+           $ret = $?;
+       };
+       my $exc_save = $@;
+       if ($exc_save)
+       {
+
+           # IPC::Run::run threw an exception. re-throw unless it's a
+           # timeout, which we'll handle by testing is_expired
+           die $exc_save
+             if (blessed($exc_save) || $exc_save !~ /^\Q$timeout_exception\E/);
+
+           $ret = undef;
+
+           die "Got timeout exception '$exc_save' but timer not expired?!"
+             unless $timeout->is_expired;
+
+           if (defined($params{timed_out}))
+           {
+               ${ $params{timed_out} } = 1;
+           }
+           else
+           {
+               die "psql timed out: stderr: '$$stderr'\n"
+                 . "while running '@psql_params'";
+           }
+       }
+   };
+
+   if (defined $$stdout)
+   {
+       chomp $$stdout;
+       $$stdout =~ s/\r//g if $TestLib::windows_os;
+   }
+
+   if (defined $$stderr)
+   {
+       chomp $$stderr;
+       $$stderr =~ s/\r//g if $TestLib::windows_os;
+   }
+
+   # See http://perldoc.perl.org/perlvar.html#%24CHILD_ERROR
+   # We don't use IPC::Run::Simple to limit dependencies.
+   #
+   # We always die on signal.
+   my $core = $ret & 128 ? " (core dumped)" : "";
+   die "psql exited with signal "
+     . ($ret & 127)
+     . "$core: '$$stderr' while running '@psql_params'"
+     if $ret & 127;
+   $ret = $ret >> 8;
+
+   if ($ret && $params{on_error_die})
+   {
+       die "psql error: stderr: '$$stderr'\nwhile running '@psql_params'"
+         if $ret == 1;
+       die "connection error: '$$stderr'\nwhile running '@psql_params'"
+         if $ret == 2;
+       die "error running SQL: '$$stderr'\nwhile running '@psql_params'"
+         if $ret == 3;
+       die "psql returns $ret: '$$stderr'\nwhile running '@psql_params'";
+   }
+
+   if (wantarray)
+   {
+       return ($ret, $$stdout, $$stderr);
+   }
+   else
+   {
+       return $ret;
+   }
+}
+
+=pod
+
+=item $node->poll_query_until(dbname, query)
+
+Run a query once a second, until it returns 't' (i.e. SQL boolean true).
+Continues polling if psql returns an error result. Times out after 180 seconds.
+
+=cut
+
+sub poll_query_until
+{
+   my ($self, $dbname, $query) = @_;
+
+   my $max_attempts = 180;
+   my $attempts     = 0;
+   my ($stdout, $stderr);
+
+   while ($attempts < $max_attempts)
+   {
+       my $cmd =
+         [ 'psql', '-XAt', '-c', $query, '-d', $self->connstr($dbname) ];
+       my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
+
+       chomp($stdout);
+       $stdout =~ s/\r//g if $TestLib::windows_os;
+       if ($stdout eq "t")
+       {
+           return 1;
+       }
+
+       # Wait a second before retrying.
+       sleep 1;
+       $attempts++;
+   }
+
+   # The query result didn't change in 180 seconds. Give up. Print the
+   # output from the last attempt, hopefully that's useful for debugging.
+   chomp($stderr);
+   $stderr =~ s/\r//g if $TestLib::windows_os;
+   diag qq(poll_query_until timed out executing this query:
+$query
+expecting this output:
+t
+last actual query output:
+$stdout
+with stderr:
+$stderr);
+   return 0;
+}
+
+=pod
+
+=item $node->command_ok(...)
+
+Runs a shell command like TestLib::command_ok, but with PGHOST and PGPORT set
+so that the command will default to connecting to this PostgresNode.
+
+=cut
+
+sub command_ok
+{
+   my $self = shift;
+
+   local $ENV{PGHOST} = $self->host;
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_ok(@_);
+}
+
+=pod
+
+=item $node->command_fails(...) - TestLib::command_fails with our PGPORT
+
+TestLib::command_fails with our connection parameters. See command_ok(...)
+
+=cut
+
+sub command_fails
+{
+   my $self = shift;
+
+   local $ENV{PGHOST} = $self->host;
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_fails(@_);
+}
+
+=pod
+
+=item $node->command_like(...)
+
+TestLib::command_like with our connection parameters. See command_ok(...)
+
+=cut
+
+sub command_like
+{
+   my $self = shift;
+
+   local $ENV{PGHOST} = $self->host;
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_like(@_);
+}
+
+=pod
+
+=item $node->issues_sql_like(cmd, expected_sql, test_name)
+
+Run a command on the node, then verify that $expected_sql appears in the
+server log file.
+
+Reads the whole log file so be careful when working with large log outputs.
+The log file is truncated prior to running the command, however.
+
+=cut
+
+sub issues_sql_like
+{
+   my ($self, $cmd, $expected_sql, $test_name) = @_;
+
+   local $ENV{PGHOST} = $self->host;
+   local $ENV{PGPORT} = $self->port;
+
+   truncate $self->logfile, 0;
+   my $result = TestLib::run_log($cmd);
+   ok($result, "@$cmd exit code 0");
+   my $log = TestLib::slurp_file($self->logfile);
+   like($log, $expected_sql, "$test_name: SQL found in server log");
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/perl/RecursiveCopy.pm b/src/test/perl/RecursiveCopy.pm
new file mode 100644 (file)
index 0000000..8be9211
--- /dev/null
@@ -0,0 +1,154 @@
+
+=pod
+
+=head1 NAME
+
+RecursiveCopy - simple recursive copy implementation
+
+=head1 SYNOPSIS
+
+use RecursiveCopy;
+
+RecursiveCopy::copypath($from, $to, filterfn => sub { return 1; });
+RecursiveCopy::copypath($from, $to);
+
+=cut
+
+package RecursiveCopy;
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 DESCRIPTION
+
+=head2 copypath($from, $to, %params)
+
+Recursively copy all files and directories from $from to $to.
+Does not preserve file metadata (e.g., permissions).
+
+Only regular files and subdirectories are copied.  Trying to copy other types
+of directory entries raises an exception.
+
+Raises an exception if a file would be overwritten, the source directory can't
+be read, or any I/O operation fails.  However, we silently ignore ENOENT on
+open, because when copying from a live database it's possible for a file/dir
+to be deleted after we see its directory entry but before we can open it.
+
+Always returns true.
+
+If the B<filterfn> parameter is given, it must be a subroutine reference.
+This subroutine will be called for each entry in the source directory with its
+relative path as only parameter; if the subroutine returns true the entry is
+copied, otherwise the file is skipped.
+
+On failure the target directory may be in some incomplete state; no cleanup is
+attempted.
+
+=head1 EXAMPLES
+
+ RecursiveCopy::copypath('/some/path', '/empty/dir',
+    filterfn => sub {
+       # omit pg_log and contents
+       my $src = shift;
+       return $src ne 'pg_log';
+   }
+ );
+
+=cut
+
+sub copypath
+{
+   my ($base_src_dir, $base_dest_dir, %params) = @_;
+   my $filterfn;
+
+   if (defined $params{filterfn})
+   {
+       die "if specified, filterfn must be a subroutine reference"
+         unless defined(ref $params{filterfn})
+             and (ref $params{filterfn} eq 'CODE');
+
+       $filterfn = $params{filterfn};
+   }
+   else
+   {
+       $filterfn = sub { return 1; };
+   }
+
+   # Complain if original path is bogus, because _copypath_recurse won't.
+   die "\"$base_src_dir\" does not exist" if !-e $base_src_dir;
+
+   # Start recursive copy from current directory
+   return _copypath_recurse($base_src_dir, $base_dest_dir, "", $filterfn);
+}
+
+# Recursive private guts of copypath
+sub _copypath_recurse
+{
+   my ($base_src_dir, $base_dest_dir, $curr_path, $filterfn) = @_;
+   my $srcpath  = "$base_src_dir/$curr_path";
+   my $destpath = "$base_dest_dir/$curr_path";
+
+   # invoke the filter and skip all further operation if it returns false
+   return 1 unless &$filterfn($curr_path);
+
+   # Check for symlink -- needed only on source dir
+   # (note: this will fall through quietly if file is already gone)
+   die "Cannot operate on symlink \"$srcpath\"" if -l $srcpath;
+
+   # Abort if destination path already exists.  Should we allow directories
+   # to exist already?
+   die "Destination path \"$destpath\" already exists" if -e $destpath;
+
+   # If this source path is a file, simply copy it to destination with the
+   # same name and we're done.
+   if (-f $srcpath)
+   {
+       my $fh;
+       unless (open($fh, '<', $srcpath))
+       {
+           return 1 if ($!{ENOENT});
+           die "open($srcpath) failed: $!";
+       }
+       copy($fh, $destpath)
+         or die "copy $srcpath -> $destpath failed: $!";
+       close $fh;
+       return 1;
+   }
+
+   # If it's a directory, create it on dest and recurse into it.
+   if (-d $srcpath)
+   {
+       my $directory;
+       unless (opendir($directory, $srcpath))
+       {
+           return 1 if ($!{ENOENT});
+           die "opendir($srcpath) failed: $!";
+       }
+
+       mkdir($destpath) or die "mkdir($destpath) failed: $!";
+
+       while (my $entry = readdir($directory))
+       {
+           next if ($entry eq '.' or $entry eq '..');
+           _copypath_recurse($base_src_dir, $base_dest_dir,
+               $curr_path eq '' ? $entry : "$curr_path/$entry", $filterfn)
+             or die "copypath $srcpath/$entry -> $destpath/$entry failed";
+       }
+
+       closedir($directory);
+       return 1;
+   }
+
+   # If it disappeared from sight, that's OK.
+   return 1 if !-e $srcpath;
+
+   # Else it's some weird file type; complain.
+   die "Source path \"$srcpath\" is not a regular file or directory";
+}
+
+1;
index 35505afe0265fbb9883dffaf4b4f471b5e9982df..b2c536b044afb85b97ea520b706251c19f5fc69d 100644 (file)
@@ -15,6 +15,7 @@ our @EXPORT = qw(
   psql
   slurp_dir
   slurp_file
+  append_to_file
   system_or_bail
   system_log
   run_log
@@ -129,6 +130,33 @@ sub tempdir_short
    return File::Temp::tempdir(CLEANUP => 1);
 }
 
+# Translate a Perl file name to a host file name.  Currently, this is a no-op
+# except for the case of Perl=msys and host=mingw32.  The subject need not
+# exist, but its parent directory must exist.
+sub perl2host
+{
+   my ($subject) = @_;
+   return $subject unless $Config{osname} eq 'msys';
+   my $here = cwd;
+   my $leaf;
+   if (chdir $subject)
+   {
+       $leaf = '';
+   }
+   else
+   {
+       $leaf = '/' . basename $subject;
+       my $parent = dirname $subject;
+       chdir $parent or die "could not chdir \"$parent\": $!";
+   }
+
+   # this odd way of calling 'pwd -W' is the only way that seems to work.
+   my $dir = qx{sh -c "pwd -W"};
+   chomp $dir;
+   chdir $here;
+   return $dir . $leaf;
+}
+
 # Initialize a new cluster for testing.
 #
 # The PGHOST environment variable is set to connect to the new cluster.
@@ -257,6 +285,15 @@ sub slurp_file
    return $contents;
 }
 
+sub append_to_file
+{
+   my ($filename, $str) = @_;
+   open my $fh, ">>", $filename
+     or die "could not write \"$filename\": $!";
+   print $fh $str;
+   close $fh;
+}
+
 sub system_or_bail
 {
    if (system_log(@_) != 0)
diff --git a/src/test/recovery/.gitignore b/src/test/recovery/.gitignore
new file mode 100644 (file)
index 0000000..871e943
--- /dev/null
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile
new file mode 100644 (file)
index 0000000..9290719
--- /dev/null
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/recovery
+#
+# Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/recovery/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/recovery
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+   $(prove_check)
+
+clean distclean maintainer-clean:
+   rm -rf tmp_check
diff --git a/src/test/recovery/README b/src/test/recovery/README
new file mode 100644 (file)
index 0000000..3cafb9d
--- /dev/null
@@ -0,0 +1,17 @@
+src/test/recovery/README
+
+Regression tests for recovery and replication
+=============================================
+
+This directory contains a test suite for recovery and replication.
+
+Running the tests
+=================
+
+    make check
+
+NOTE: This creates a temporary installation, and some tests may
+create one or multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This requires the --enable-tap-tests argument to configure.
diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
new file mode 100644 (file)
index 0000000..981c00b
--- /dev/null
@@ -0,0 +1,68 @@
+# Minimal test testing streaming replication
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 4;
+
+# Initialize master node
+my $node_master = get_new_node('master');
+$node_master->init(allows_streaming => 1);
+$node_master->start;
+my $backup_name = 'my_backup';
+
+# Take backup
+$node_master->backup($backup_name);
+
+# Create streaming standby linking to master
+my $node_standby_1 = get_new_node('standby_1');
+$node_standby_1->init_from_backup($node_master, $backup_name,
+   has_streaming => 1);
+$node_standby_1->start;
+
+# Take backup of standby 1 (not mandatory, but useful to check if
+# pg_basebackup works on a standby).
+$node_standby_1->backup($backup_name);
+
+# Take a second backup of the standby while the master is offline.
+$node_master->stop;
+$node_standby_1->backup('my_backup_2');
+$node_master->start;
+
+# Create second standby node linking to standby 1
+my $node_standby_2 = get_new_node('standby_2');
+$node_standby_2->init_from_backup($node_standby_1, $backup_name,
+   has_streaming => 1);
+$node_standby_2->start;
+
+# Create some content on master and check its presence in standby 1
+$node_master->safe_psql('postgres',
+   "CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a");
+
+# Wait for standbys to catch up
+my $applname_1 = $node_standby_1->name;
+my $applname_2 = $node_standby_2->name;
+my $caughtup_query =
+"SELECT pg_current_xlog_location() <= replay_location FROM pg_stat_replication WHERE application_name = '$applname_1';";
+$node_master->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for standby 1 to catch up";
+$caughtup_query =
+"SELECT pg_last_xlog_replay_location() <= replay_location FROM pg_stat_replication WHERE application_name = '$applname_2';";
+$node_standby_1->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for standby 2 to catch up";
+
+my $result =
+  $node_standby_1->safe_psql('postgres', "SELECT count(*) FROM tab_int");
+print "standby 1: $result\n";
+is($result, qq(1002), 'check streamed content on standby 1');
+
+$result =
+  $node_standby_2->safe_psql('postgres', "SELECT count(*) FROM tab_int");
+print "standby 2: $result\n";
+is($result, qq(1002), 'check streamed content on standby 2');
+
+# Check that only READ-only queries can run on standbys
+is($node_standby_1->psql('postgres', 'INSERT INTO tab_int VALUES (1)'),
+   3, 'read-only queries on standby 1');
+is($node_standby_2->psql('postgres', 'INSERT INTO tab_int VALUES (1)'),
+   3, 'read-only queries on standby 2');
diff --git a/src/test/recovery/t/002_archiving.pl b/src/test/recovery/t/002_archiving.pl
new file mode 100644 (file)
index 0000000..c0ce608
--- /dev/null
@@ -0,0 +1,76 @@
+# test for archiving with hot standby
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 3;
+use File::Copy;
+
+# Initialize master node, doing archives
+my $node_master = get_new_node('master');
+$node_master->init(
+   has_archiving    => 1,
+   allows_streaming => 1);
+my $backup_name = 'my_backup';
+
+# Start it
+$node_master->start;
+
+# Take backup for slave
+$node_master->backup($backup_name);
+
+# Initialize standby node from backup, fetching WAL from archives
+my $node_standby = get_new_node('standby');
+$node_standby->init_from_backup($node_master, $backup_name,
+   has_restoring => 1);
+$node_standby->append_conf(
+   'postgresql.conf', qq(
+wal_retrieve_retry_interval = '100ms'
+));
+$node_standby->start;
+
+# Create some content on master
+$node_master->safe_psql('postgres',
+   "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a");
+my $current_lsn =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+
+# Force archiving of WAL file to make it present on master
+$node_master->safe_psql('postgres', "SELECT pg_switch_xlog()");
+
+# Add some more content, it should not be present on standby
+$node_master->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(1001,2000))");
+
+# Wait until necessary replay has been done on standby
+my $caughtup_query =
+  "SELECT '$current_lsn'::pg_lsn <= pg_last_xlog_replay_location()";
+$node_standby->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for standby to catch up";
+
+my $result =
+  $node_standby->safe_psql('postgres', "SELECT count(*) FROM tab_int");
+is($result, qq(1000), 'check content from archives');
+
+# Check the presence of temporary files specifically generated during
+# archive recovery.  To ensure the presence of the temporary history
+# file, switch to a timeline large enough to allow a standby to recover
+# a history file from an archive.  As this requires at least two timeline
+# switches, promote the existing standby first.  Then create a second
+# standby based on the promoted one.  Finally, the second standby is
+# promoted.
+$node_standby->promote;
+
+my $node_standby2 = get_new_node('standby2');
+$node_standby2->init_from_backup($node_master, $backup_name,
+   has_restoring => 1);
+$node_standby2->start;
+
+# Now promote standby2, and check that temporary files specifically
+# generated during archive recovery are removed by the end of recovery.
+$node_standby2->promote;
+my $node_standby2_data = $node_standby2->data_dir;
+ok( !-f "$node_standby2_data/pg_wal/RECOVERYHISTORY",
+   "RECOVERYHISTORY removed after promotion");
+ok( !-f "$node_standby2_data/pg_wal/RECOVERYXLOG",
+   "RECOVERYXLOG removed after promotion");
diff --git a/src/test/recovery/t/003_recovery_targets.pl b/src/test/recovery/t/003_recovery_targets.pl
new file mode 100644 (file)
index 0000000..fd75952
--- /dev/null
@@ -0,0 +1,127 @@
+# Test for recovery targets: name, timestamp, XID
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 7;
+
+# Create and test a standby from given backup, with a certain recovery target.
+# Choose $until_lsn later than the transaction commit that causes the row
+# count to reach $num_rows, yet not later than the recovery target.
+sub test_recovery_standby
+{
+   my $test_name       = shift;
+   my $node_name       = shift;
+   my $node_master     = shift;
+   my $recovery_params = shift;
+   my $num_rows        = shift;
+   my $until_lsn       = shift;
+
+   my $node_standby = get_new_node($node_name);
+   $node_standby->init_from_backup($node_master, 'my_backup',
+       has_restoring => 1);
+
+   foreach my $param_item (@$recovery_params)
+   {
+       $node_standby->append_conf(
+           'recovery.conf',
+           qq($param_item
+));
+   }
+
+   $node_standby->start;
+
+   # Wait until standby has replayed enough data
+   my $caughtup_query =
+     "SELECT '$until_lsn'::pg_lsn <= pg_last_xlog_replay_location()";
+   $node_standby->poll_query_until('postgres', $caughtup_query)
+     or die "Timed out while waiting for standby to catch up";
+
+   # Create some content on master and check its presence in standby
+   my $result =
+     $node_standby->safe_psql('postgres', "SELECT count(*) FROM tab_int");
+   is($result, qq($num_rows), "check standby content for $test_name");
+
+   # Stop standby node
+   $node_standby->teardown_node;
+}
+
+# Initialize master node
+my $node_master = get_new_node('master');
+$node_master->init(has_archiving => 1, allows_streaming => 1);
+
+# Start it
+$node_master->start;
+
+# Create data before taking the backup, aimed at testing
+# recovery_target = 'immediate'
+$node_master->safe_psql('postgres',
+   "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a");
+my $lsn1 =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+
+# Take backup from which all operations will be run
+$node_master->backup('my_backup');
+
+# Insert some data with used as a replay reference, with a recovery
+# target TXID.
+$node_master->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(1001,2000))");
+my $ret = $node_master->safe_psql('postgres',
+   "SELECT pg_current_xlog_location(), txid_current();");
+my ($lsn2, $recovery_txid) = split /\|/, $ret;
+
+# More data, with recovery target timestamp
+$node_master->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(2001,3000))");
+my $lsn3 =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+my $recovery_time = $node_master->safe_psql('postgres', "SELECT now()");
+
+# Even more data, this time with a recovery target name
+$node_master->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(3001,4000))");
+my $recovery_name = "my_target";
+my $lsn4 =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+$node_master->safe_psql('postgres',
+   "SELECT pg_create_restore_point('$recovery_name');");
+
+# Force archiving of WAL file
+$node_master->safe_psql('postgres', "SELECT pg_switch_xlog()");
+
+# Test recovery targets
+my @recovery_params = ("recovery_target = 'immediate'");
+test_recovery_standby('immediate target',
+   'standby_1', $node_master, \@recovery_params, "1000", $lsn1);
+@recovery_params = ("recovery_target_xid = '$recovery_txid'");
+test_recovery_standby('XID', 'standby_2', $node_master, \@recovery_params,
+   "2000", $lsn2);
+@recovery_params = ("recovery_target_time = '$recovery_time'");
+test_recovery_standby('time', 'standby_3', $node_master, \@recovery_params,
+   "3000", $lsn3);
+@recovery_params = ("recovery_target_name = '$recovery_name'");
+test_recovery_standby('name', 'standby_4', $node_master, \@recovery_params,
+   "4000", $lsn4);
+
+# Multiple targets
+# Last entry has priority (note that an array respects the order of items
+# not hashes).
+@recovery_params = (
+   "recovery_target_name = '$recovery_name'",
+   "recovery_target_xid  = '$recovery_txid'",
+   "recovery_target_time = '$recovery_time'");
+test_recovery_standby('name + XID + time',
+   'standby_5', $node_master, \@recovery_params, "3000", $lsn3);
+@recovery_params = (
+   "recovery_target_time = '$recovery_time'",
+   "recovery_target_name = '$recovery_name'",
+   "recovery_target_xid  = '$recovery_txid'");
+test_recovery_standby('time + name + XID',
+   'standby_6', $node_master, \@recovery_params, "2000", $lsn2);
+@recovery_params = (
+   "recovery_target_xid  = '$recovery_txid'",
+   "recovery_target_time = '$recovery_time'",
+   "recovery_target_name = '$recovery_name'");
+test_recovery_standby('XID + time + name',
+   'standby_7', $node_master, \@recovery_params, "4000", $lsn4);
diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl
new file mode 100644 (file)
index 0000000..3ee8df2
--- /dev/null
@@ -0,0 +1,75 @@
+# Test for timeline switch
+# Ensure that a cascading standby is able to follow a newly-promoted standby
+# on a new timeline.
+use strict;
+use warnings;
+use File::Path qw(rmtree);
+use PostgresNode;
+use TestLib;
+use Test::More tests => 1;
+
+$ENV{PGDATABASE} = 'postgres';
+
+# Initialize master node
+my $node_master = get_new_node('master');
+$node_master->init(allows_streaming => 1);
+$node_master->start;
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_master->backup($backup_name);
+
+# Create two standbys linking to it
+my $node_standby_1 = get_new_node('standby_1');
+$node_standby_1->init_from_backup($node_master, $backup_name,
+   has_streaming => 1);
+$node_standby_1->start;
+my $node_standby_2 = get_new_node('standby_2');
+$node_standby_2->init_from_backup($node_master, $backup_name,
+   has_streaming => 1);
+$node_standby_2->start;
+
+# Create some content on master
+$node_master->safe_psql('postgres',
+   "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a");
+my $until_lsn =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+
+# Wait until standby has replayed enough data on standby 1
+my $caughtup_query =
+  "SELECT '$until_lsn'::pg_lsn <= pg_last_xlog_replay_location()";
+$node_standby_1->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for standby to catch up";
+
+# Stop and remove master, and promote standby 1, switching it to a new timeline
+$node_master->teardown_node;
+$node_standby_1->promote;
+
+# Switch standby 2 to replay from standby 1
+rmtree($node_standby_2->data_dir . '/recovery.conf');
+my $connstr_1 = $node_standby_1->connstr;
+$node_standby_2->append_conf(
+   'recovery.conf', qq(
+primary_conninfo='$connstr_1'
+standby_mode=on
+recovery_target_timeline='latest'
+));
+$node_standby_2->restart;
+
+# Insert some data in standby 1 and check its presence in standby 2
+# to ensure that the timeline switch has been done. Standby 1 needs
+# to exit recovery first before moving on with the test.
+$node_standby_1->poll_query_until('postgres',
+   "SELECT pg_is_in_recovery() <> true");
+$node_standby_1->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(1001,2000))");
+$until_lsn = $node_standby_1->safe_psql('postgres',
+   "SELECT pg_current_xlog_location();");
+$caughtup_query =
+  "SELECT '$until_lsn'::pg_lsn <= pg_last_xlog_replay_location()";
+$node_standby_2->poll_query_until('postgres', $caughtup_query)
+  or die "Timed out while waiting for standby to catch up";
+
+my $result =
+  $node_standby_2->safe_psql('postgres', "SELECT count(*) FROM tab_int");
+is($result, qq(2000), 'check content of standby 2');
diff --git a/src/test/recovery/t/005_replay_delay.pl b/src/test/recovery/t/005_replay_delay.pl
new file mode 100644 (file)
index 0000000..640295b
--- /dev/null
@@ -0,0 +1,69 @@
+# Checks for recovery_min_apply_delay
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 1;
+
+# Initialize master node
+my $node_master = get_new_node('master');
+$node_master->init(allows_streaming => 1);
+$node_master->start;
+
+# And some content
+$node_master->safe_psql('postgres',
+   "CREATE TABLE tab_int AS SELECT generate_series(1, 10) AS a");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_master->backup($backup_name);
+
+# Create streaming standby from backup
+my $node_standby = get_new_node('standby');
+my $delay        = 3;
+$node_standby->init_from_backup($node_master, $backup_name,
+   has_streaming => 1);
+$node_standby->append_conf(
+   'recovery.conf', qq(
+recovery_min_apply_delay = '${delay}s'
+));
+$node_standby->start;
+
+# Make new content on master and check its presence in standby depending
+# on the delay applied above. Before doing the insertion, get the
+# current timestamp that will be used as a comparison base. Even on slow
+# machines, this allows to have a predictable behavior when comparing the
+# delay between data insertion moment on master and replay time on standby.
+my $master_insert_time = time();
+$node_master->safe_psql('postgres',
+   "INSERT INTO tab_int VALUES (generate_series(11, 20))");
+
+# Now wait for replay to complete on standby. We're done waiting when the
+# slave has replayed up to the previously saved master LSN.
+my $until_lsn =
+  $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location()");
+
+my $remaining = 90;
+while ($remaining-- > 0)
+{
+
+   # Done waiting?
+   my $replay_status = $node_standby->safe_psql('postgres',
+       "SELECT (pg_last_xlog_replay_location() - '$until_lsn'::pg_lsn) >= 0"
+   );
+   last if $replay_status eq 't';
+
+   # No, sleep some more.
+   my $sleep = $master_insert_time + $delay - time();
+   $sleep = 1 if $sleep < 1;
+   sleep $sleep;
+}
+
+die "Maximum number of attempts reached ($remaining remain)"
+  if $remaining < 0;
+
+# This test is successful if and only if the LSN has been applied with at least
+# the configured apply delay.
+ok(time() - $master_insert_time >= $delay,
+   "standby applies WAL only after replication delay");
diff --git a/src/test/recovery/t/008_fsm_truncation.pl b/src/test/recovery/t/008_fsm_truncation.pl
new file mode 100644 (file)
index 0000000..6f53c1c
--- /dev/null
@@ -0,0 +1,93 @@
+# Test WAL replay of FSM changes.
+#
+# FSM changes don't normally need to be WAL-logged, except for truncation.
+# The FSM mustn't return a page that doesn't exist (anymore).
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More tests => 1;
+
+my $node_master = get_new_node('master');
+$node_master->init(allows_streaming => 1);
+
+$node_master->append_conf('postgresql.conf', qq{
+fsync = on
+wal_level = hot_standby
+wal_log_hints = on
+max_prepared_transactions = 5
+autovacuum = off
+});
+
+# Create a master node and its standby, initializing both with some data
+# at the same time.
+$node_master->start;
+
+$node_master->backup('master_backup');
+my $node_standby = get_new_node('standby');
+$node_standby->init_from_backup($node_master, 'master_backup',
+   has_streaming => 1);
+$node_standby->start;
+
+$node_master->psql('postgres', qq{
+create table testtab (a int, b char(100));
+insert into testtab select generate_series(1,1000), 'foo';
+insert into testtab select generate_series(1,1000), 'foo';
+delete from testtab where ctid > '(8,0)';
+});
+
+# Take a lock on the table to prevent following vacuum from truncating it
+$node_master->psql('postgres', qq{
+begin;
+lock table testtab in row share mode;
+prepare transaction 'p1';
+});
+
+# Vacuum, update FSM without truncation
+$node_master->psql('postgres', 'vacuum verbose testtab');
+
+# Force a checkpoint
+$node_master->psql('postgres', 'checkpoint');
+
+# Now do some more insert/deletes, another vacuum to ensure full-page writes
+# are done
+$node_master->psql('postgres', qq{
+insert into testtab select generate_series(1,1000), 'foo';
+delete from testtab where ctid > '(8,0)';
+vacuum verbose testtab;
+});
+
+# Ensure all buffers are now clean on the standby
+$node_standby->psql('postgres', 'checkpoint');
+
+# Release the lock, vacuum again which should lead to truncation
+$node_master->psql('postgres', qq{
+rollback prepared 'p1';
+vacuum verbose testtab;
+});
+
+$node_master->psql('postgres', 'checkpoint');
+my $until_lsn =
+   $node_master->safe_psql('postgres', "SELECT pg_current_xlog_location();");
+
+# Wait long enough for standby to receive and apply all WAL
+my $caughtup_query =
+   "SELECT '$until_lsn'::pg_lsn <= pg_last_xlog_replay_location()";
+$node_standby->poll_query_until('postgres', $caughtup_query)
+   or die "Timed out while waiting for standby to catch up";
+
+# Promote the standby
+$node_standby->promote;
+$node_standby->poll_query_until('postgres',
+   "SELECT NOT pg_is_in_recovery()")
+  or die "Timed out while waiting for promotion of standby";
+$node_standby->psql('postgres', 'checkpoint');
+
+# Restart to discard in-memory copy of FSM
+$node_standby->restart;
+
+# Insert should work on standby
+is($node_standby->psql('postgres',
+   qq{insert into testtab select generate_series(1,1000), 'foo';}),
+   0, 'INSERT succeeds with truncated relation FSM');
diff --git a/src/test/recovery/t/017_shm.pl b/src/test/recovery/t/017_shm.pl
new file mode 100644 (file)
index 0000000..634b032
--- /dev/null
@@ -0,0 +1,204 @@
+#
+# Tests of pg_shmem.h functions
+#
+use strict;
+use warnings;
+use IPC::Run 'run';
+use PostgresNode;
+use Test::More;
+use TestLib;
+use Time::HiRes qw(usleep);
+
+if ($windows_os)
+{
+   plan skip_all => 'SysV shared memory not supported by this platform';
+}
+else
+{
+   plan tests => 5;
+}
+
+my $tempdir = TestLib::tempdir;
+my $port;
+
+# Log "ipcs" diffs on a best-effort basis, swallowing any error.
+my $ipcs_before = "$tempdir/ipcs_before";
+eval { run_log [ 'ipcs', '-am' ], '>', $ipcs_before; };
+
+sub log_ipcs
+{
+   eval { run_log [ 'ipcs', '-am' ], '|', [ 'diff', $ipcs_before, '-' ] };
+   return;
+}
+
+# These tests need a $port such that nothing creates or removes a segment in
+# $port's IpcMemoryKey range while this test script runs.  While there's no
+# way to ensure that in general, we do ensure that if PostgreSQL tests are the
+# only actors.  With TCP, the first get_new_node picks a port number.  With
+# Unix sockets, use a postmaster, $port_holder, to represent a key space
+# reservation.  $port_holder holds a reservation on the key space of port
+# 1+$port_holder->port if it created the first IpcMemoryKey of its own port's
+# key space.  If multiple copies of this test script run concurrently, they
+# will pick different ports.  $port_holder postmasters use odd-numbered ports,
+# and tests use even-numbered ports.  In the absence of collisions from other
+# shmget() activity, gnat starts with key 0x7d001 (512001), and flea starts
+# with key 0x7d002 (512002).
+my $port_holder;
+if (!$PostgresNode::use_tcp)
+{
+   my $lock_port;
+   for ($lock_port = 511; $lock_port < 711; $lock_port += 2)
+   {
+       $port_holder = PostgresNode->get_new_node(
+           "port${lock_port}_holder",
+           port     => $lock_port,
+           own_host => 1);
+       $port_holder->init(hba_permit_replication => 0);
+       $port_holder->append_conf('postgresql.conf', 'max_connections = 5');
+       $port_holder->start;
+       # Match the AddToDataDirLockFile() call in sysv_shmem.c.  Assume all
+       # systems not using sysv_shmem.c do use TCP.
+       my $shmem_key_line_prefix = sprintf("%9lu ", 1 + $lock_port * 1000);
+       last
+         if slurp_file($port_holder->data_dir . '/postmaster.pid') =~
+         /^$shmem_key_line_prefix/m;
+       $port_holder->stop;
+   }
+   $port = $lock_port + 1;
+}
+
+# Node setup.
+sub init_start
+{
+   my $name = shift;
+   my $ret = PostgresNode->get_new_node($name, port => $port, own_host => 1);
+   defined($port) or $port = $ret->port;    # same port for all nodes
+   $ret->init(hba_permit_replication => 0);
+   # Limit semaphore consumption, since we run several nodes concurrently.
+   $ret->append_conf('postgresql.conf', 'max_connections = 5');
+   $ret->start;
+   log_ipcs();
+   return $ret;
+}
+my $gnat = init_start 'gnat';
+my $flea = init_start 'flea';
+
+# Upon postmaster death, postmaster children exit automatically.
+$gnat->kill9;
+log_ipcs();
+$flea->restart;       # flea ignores the shm key gnat abandoned.
+log_ipcs();
+poll_start($gnat);    # gnat recycles its former shm key.
+log_ipcs();
+
+# After clean shutdown, the nodes swap shm keys.
+$gnat->stop;
+$flea->restart;
+log_ipcs();
+$gnat->start;
+log_ipcs();
+
+# Scenarios involving no postmaster.pid, dead postmaster, and a live backend.
+# Use a regress.c function to emulate the responsiveness of a backend working
+# through a CPU-intensive task.
+$gnat->safe_psql('postgres', <<EOSQL);
+CREATE FUNCTION wait_pid(int)
+   RETURNS void
+   AS '$ENV{REGRESS_SHLIB}'
+   LANGUAGE C STRICT;
+EOSQL
+my $slow_query = 'SELECT wait_pid(pg_backend_pid())';
+my ($stdout, $stderr);
+my $slow_client = IPC::Run::start(
+   [
+       'psql', '-X', '-qAt', '-d', $gnat->connstr('postgres'),
+       '-c', $slow_query
+   ],
+   '<',
+   \undef,
+   '>',
+   \$stdout,
+   '2>',
+   \$stderr,
+   IPC::Run::timeout(900));    # five times the poll_query_until timeout
+ok( $gnat->poll_query_until(
+       'postgres',
+       "SELECT true FROM pg_stat_activity WHERE query = '$slow_query'"),
+   'slow query started');
+my $slow_pid = $gnat->safe_psql('postgres',
+   "SELECT pid FROM pg_stat_activity WHERE query = '$slow_query'");
+$gnat->kill9;
+unlink($gnat->data_dir . '/postmaster.pid');
+$gnat->rotate_logfile;
+log_ipcs();
+# Reject ordinary startup.  Retry for the same reasons poll_start() does.
+my $pre_existing_msg = qr/pre-existing shared memory block/;
+{
+   my $max_attempts = 180 * 10;    # Retry every 0.1s for at least 180s.
+   my $attempts     = 0;
+   while ($attempts < $max_attempts)
+   {
+       last
+         if $gnat->start(fail_ok => 1)
+         || slurp_file($gnat->logfile) =~ $pre_existing_msg;
+       usleep(100_000);
+       $attempts++;
+   }
+}
+like(slurp_file($gnat->logfile),
+   $pre_existing_msg, 'detected live backend via shared memory');
+# Reject single-user startup.
+my $single_stderr;
+ok( !run_log(
+       [ 'postgres', '--single', '-D', $gnat->data_dir, 'template1' ],
+       '<', \undef, '2>', \$single_stderr),
+   'live query blocks --single');
+print STDERR $single_stderr;
+like($single_stderr, $pre_existing_msg,
+   'single-user mode detected live backend via shared memory');
+log_ipcs();
+# Fail to reject startup if shm key N has become available and we crash while
+# using key N+1.  This is unwanted, but expected.
+$flea->stop;    # release first key
+is($gnat->start(fail_ok => 1), 1, 'key turnover fools only sysv_shmem.c');
+$gnat->stop;     # release first key
+$flea->start;    # grab first key
+# cleanup
+TestLib::system_log('pg_ctl', 'kill', 'QUIT', $slow_pid);
+$slow_client->finish;    # client has detected backend termination
+log_ipcs();
+poll_start($gnat);       # recycle second key
+
+$gnat->stop;
+$flea->stop;
+$port_holder->stop if $port_holder;
+log_ipcs();
+
+
+# We may need retries to start a new postmaster.  Causes:
+# - kernel is slow to deliver SIGKILL
+# - postmaster parent is slow to waitpid()
+# - postmaster child is slow to exit in response to SIGQUIT
+# - postmaster child is slow to exit after postmaster death
+sub poll_start
+{
+   my ($node) = @_;
+
+   my $max_attempts = 180 * 10;
+   my $attempts     = 0;
+
+   while ($attempts < $max_attempts)
+   {
+       $node->start(fail_ok => 1) && return 1;
+
+       # Wait 0.1 second before retrying.
+       usleep(100_000);
+
+       $attempts++;
+   }
+
+   # No success within 180 seconds.  Try one last time without fail_ok, which
+   # will BAIL_OUT unless it succeeds.
+   $node->start && return 1;
+   return 0;
+}
index 31d064f26fbb6d5e992c055a59f57ba9c790edb6..1a9a282494f6ac7c9b8c7c41fc4d2f4aa94a0a32 100644 (file)
@@ -37,7 +37,7 @@ if (-e "src/tools/msvc/buildenv.pl")
 
 my $what = shift || "";
 if ($what =~
-/^(check|installcheck|plcheck|contribcheck|modulescheck|ecpgcheck|isolationcheck|upgradecheck|bincheck|taptest)$/i
+/^(check|installcheck|plcheck|contribcheck|modulescheck|ecpgcheck|isolationcheck|upgradecheck|bincheck|recoverycheck|taptest)$/i
   )
 {
    $what = uc $what;
@@ -85,6 +85,7 @@ my %command = (
    MODULESCHECK   => \&modulescheck,
    ISOLATIONCHECK => \&isolationcheck,
    BINCHECK       => \&bincheck,
+   RECOVERYCHECK  => \&recoverycheck,
    UPGRADECHECK   => \&upgradecheck,
    TAPTEST        => \&taptest,);
 
@@ -203,8 +204,9 @@ sub tap_check
 
    # adjust the environment for just this test
    local %ENV = %ENV;
-   $ENV{PERL5LIB} = "$topdir/src/test/perl;$ENV{PERL5LIB}";
+   $ENV{PERL5LIB}   = "$topdir/src/test/perl;$ENV{PERL5LIB}";
    $ENV{PG_REGRESS} = "$topdir/$Config/pg_regress/pg_regress";
+   $ENV{REGRESS_SHLIB} = "$topdir/src/test/regress/regress.dll";
 
    $ENV{TESTDIR} = "$dir";
 
@@ -439,6 +441,16 @@ sub modulescheck
    exit $mstat if $mstat;
 }
 
+sub recoverycheck
+{
+   InstallTemp();
+
+   my $mstat  = 0;
+   my $dir    = "$topdir/src/test/recovery";
+   my $status = tap_check($dir);
+   exit $status if $status;
+}
+
 # Run "initdb", then reconfigure authentication.
 sub standard_initdb
 {
@@ -683,6 +695,7 @@ sub usage
      "  isolationcheck run isolation tests\n",
      "  modulescheck   run tests of modules in src/test/modules/\n",
      "  plcheck        run tests of PL languages\n",
+     "  recoverycheck  run recovery test suite\n",
      "  taptest        run an arbitrary TAP test set\n",
      "  upgradecheck   run tests of pg_upgrade\n",
      "\nOptions for <arg>: (used by check and installcheck)\n",