aboutsummaryrefslogtreecommitdiff
path: root/src/test/perl/PostgresNode.pm
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/perl/PostgresNode.pm')
-rw-r--r--src/test/perl/PostgresNode.pm2811
1 files changed, 0 insertions, 2811 deletions
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
deleted file mode 100644
index 465fdb68708..00000000000
--- a/src/test/perl/PostgresNode.pm
+++ /dev/null
@@ -1,2811 +0,0 @@
-
-# Copyright (c) 2021, PostgreSQL Global Development Group
-
-=pod
-
-=head1 NAME
-
-PostgresNode - class representing PostgreSQL server instance
-
-=head1 SYNOPSIS
-
- use PostgresNode;
-
- my $node = PostgresNode->new('mynode');
-
- # Create a data directory with initdb
- $node->init();
-
- # Start the PostgreSQL server
- $node->start();
-
- # Add a setting and restart
- $node->append_conf('postgresql.conf', 'hot_standby = on');
- $node->restart();
-
- # Modify or delete an existing setting
- $node->adjust_conf('postgresql.conf', 'max_wal_senders', '10');
-
- # 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(600)',
- stdout => \$stdout, stderr => \$stderr,
- timeout => 180, 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 die "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 $other_node = PostgresNode->new('mycopy');
- $other_node->init_from_backup($node, 'testbackup');
- $other_node->start;
-
- # Stop the server
- $node->stop('fast');
-
- # Find a free, unprivileged TCP port to bind some other service to
- my $port = PostgresNode::get_free_port();
-
-=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 Carp;
-use Config;
-use Cwd;
-use Fcntl qw(:mode);
-use File::Basename;
-use File::Path qw(rmtree);
-use File::Spec;
-use File::stat qw(stat);
-use File::Temp ();
-use IPC::Run;
-use PostgresVersion;
-use RecursiveCopy;
-use Socket;
-use Test::More;
-use TestLib ();
-use Time::HiRes qw(usleep);
-use Scalar::Util qw(blessed);
-
-our ($use_tcp, $test_localhost, $test_pghost, $last_host_assigned,
- $last_port_assigned, @all_nodes, $died);
-
-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::use_unix_sockets;
- $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 $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";
- }
-
- # Escape properly the database string before using it, only
- # single quotes and backslashes need to be treated this way.
- $dbname =~ s#\\#\\\\#g;
- $dbname =~ s#\'#\\\'#g;
-
- return "port=$pgport host=$pghost dbname='$dbname'";
-}
-
-=pod
-
-=item $node->group_access()
-
-Does the data dir allow group access?
-
-=cut
-
-sub group_access
-{
- my ($self) = @_;
-
- my $dir_stat = stat($self->data_dir);
-
- defined($dir_stat)
- or die('unable to stat ' . $self->data_dir);
-
- return (S_IMODE($dir_stat->mode) == 0750);
-}
-
-=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->install_path()
-
-The configured install path (if any) for the node.
-
-=cut
-
-sub install_path
-{
- my ($self) = @_;
- return $self->{_install_path};
-}
-
-=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 "Version: " . $self->{_pg_version} . "\n"
- if $self->{_pg_version};
- 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";
- print $fh "Install Path: ", $self->{_install_path} . "\n"
- if $self->{_install_path};
- close $fh or die;
- return $_info;
-}
-
-=pod
-
-=item $node->dump_info()
-
-Print $node->info()
-
-=cut
-
-sub dump_info
-{
- my ($self) = @_;
- print $self->info;
- return;
-}
-
-
-# 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 croak "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 && !$TestLib::use_unix_sockets)
- {
- print $hba
- "host replication all $test_localhost/32 sspi include_realm=1 map=regress\n";
- }
- close $hba;
- return;
-}
-
-=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).
-
-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 => 'logical' or 'physical' (passing 1 will also
-suffice for physical replication) depending on type of replication that
-should be enabled. 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;
-
- local %ENV = $self->_get_env();
-
- $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',
- @{ $params{extra} });
- TestLib::system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata,
- @{ $params{auth_extra} });
-
- 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_line_prefix = '%m [%p] %q%a '\n";
- print $conf "log_statement = all\n";
- print $conf "log_replication_commands = on\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})
- {
- if ($params{allows_streaming} eq "logical")
- {
- print $conf "wal_level = logical\n";
- }
- else
- {
- print $conf "wal_level = replica\n";
- }
- print $conf "max_wal_senders = 10\n";
- print $conf "max_replication_slots = 10\n";
- print $conf "wal_log_hints = on\n";
- print $conf "hot_standby = on\n";
- # conservative settings to ensure we can run multiple postmasters:
- print $conf "shared_buffers = 1MB\n";
- print $conf "max_connections = 10\n";
- # limit disk space consumption, too:
- print $conf "max_wal_size = 128MB\n";
- }
- else
- {
- print $conf "wal_level = minimal\n";
- print $conf "max_wal_senders = 0\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;
-
- chmod($self->group_access ? 0640 : 0600, "$pgdata/postgresql.conf")
- or die("unable to set permissions for $pgdata/postgresql.conf");
-
- $self->set_replication_conf if $params{allows_streaming};
- $self->enable_archiving if $params{has_archiving};
- return;
-}
-
-=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");
-
- chmod($self->group_access() ? 0640 : 0600, $conffile)
- or die("unable to set permissions for $conffile");
-
- return;
-}
-
-=pod
-
-=item $node->adjust_conf(filename, setting, value, skip_equals)
-
-Modify the named config file setting with the value. If the value is undefined,
-instead delete the setting. If the setting is not present no action is taken.
-
-This will write "$setting = $value\n" in place of the existing line,
-unless skip_equals is true, in which case it will write
-"$setting $value\n". If the value needs to be quoted it is the caller's
-responsibility to do that.
-
-=cut
-
-sub adjust_conf
-{
- my ($self, $filename, $setting, $value, $skip_equals) = @_;
-
- my $conffile = $self->data_dir . '/' . $filename;
-
- my $contents = TestLib::slurp_file($conffile);
- my @lines = split(/\n/, $contents);
- my @result;
- my $eq = $skip_equals ? '' : '= ';
- foreach my $line (@lines)
- {
- if ($line !~ /^$setting\W/)
- {
- push(@result, "$line\n");
- }
- elsif (defined $value)
- {
- push(@result, "$setting $eq$value\n");
- }
- }
- open my $fh, ">", $conffile
- or croak "could not write \"$conffile\": $!";
- print $fh @result;
- close $fh;
-
- chmod($self->group_access() ? 0640 : 0600, $conffile)
- or die("unable to set permissions for $conffile");
-}
-
-=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 WAL.
-
-By default, WAL files are fetched at the end of the backup, not streamed.
-You can adjust that and other things by passing an array of additional
-B<pg_basebackup> command line options in the keyword parameter backup_options.
-
-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, %params) = @_;
- my $backup_path = $self->backup_dir . '/' . $backup_name;
- my $name = $self->name;
-
- local %ENV = $self->_get_env();
-
- 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, '--checkpoint',
- 'fast', '--no-sync',
- @{ $params{backup_options} });
- print "# Backup finished\n";
- return;
-}
-
-=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 WAL.
-
-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);
- return;
-}
-
-=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 WAL. 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);
- return;
-}
-
-
-# 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 '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";
- return;
-}
-
-
-
-=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.
-
-By default, the backup is assumed to be plain format. To restore from
-a tar-format backup, pass the name of the tar program to use in the
-keyword parameter tar_program. Note that tablespace tar files aren't
-handled here.
-
-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.
-
-If has_restoring is used, standby mode is used by default. To use
-recovery mode instead, pass the keyword parameter standby => 0.
-
-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{has_restoring} = 0 unless defined $params{has_restoring};
- $params{standby} = 1 unless defined $params{standby};
-
- print
- "# Initializing node \"$node_name\" from backup \"$backup_name\" of node \"$root_name\"\n";
- croak "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;
- if (defined $params{tar_program})
- {
- mkdir($data_path);
- TestLib::system_or_bail($params{tar_program}, 'xf',
- $backup_path . '/base.tar',
- '-C', $data_path);
- TestLib::system_or_bail(
- $params{tar_program}, 'xf',
- $backup_path . '/pg_wal.tar', '-C',
- $data_path . '/pg_wal');
- }
- else
- {
- 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->enable_streaming($root_node) if $params{has_streaming};
- $self->enable_restoring($root_node, $params{standby})
- if $params{has_restoring};
- return;
-}
-
-=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 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;
- my $ret;
-
- BAIL_OUT("node \"$name\" is already running") if defined $self->{_pid};
-
- print("### Starting node \"$name\"\n");
-
- # Temporarily unset PGAPPNAME so that the server doesn't
- # inherit it. Otherwise this could affect libpqwalreceiver
- # connections in confusing ways.
- local %ENV = $self->_get_env(PGAPPNAME => undef);
-
- # Note: We set the cluster_name here, not in postgresql.conf (in
- # sub init) so that it does not get copied to standbys.
- # -w is now the default but having it here does no harm and helps
- # compatibility with older versions.
- $ret = TestLib::system_log('pg_ctl', '-w', '-D', $self->data_dir, '-l',
- $self->logfile, '-o', "--cluster-name=$name", '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};
-
- local %ENV = $self->_get_env();
-
- print "### Killing node \"$name\" using signal 9\n";
- # kill(9, ...) fails under msys Perl 5.8.8, so fall back on pg_ctl.
- kill(9, $self->{_pid})
- or TestLib::system_or_bail('pg_ctl', 'kill', 'KILL', $self->{_pid});
- $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;
-
- local %ENV = $self->_get_env();
-
- $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);
- return;
-}
-
-=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;
-
- local %ENV = $self->_get_env();
-
- print "### Reloading node \"$name\"\n";
- TestLib::system_or_bail('pg_ctl', '-D', $pgdata, 'reload');
- return;
-}
-
-=pod
-
-=item $node->restart()
-
-Wrapper for pg_ctl restart
-
-=cut
-
-sub restart
-{
- my ($self) = @_;
- my $port = $self->port;
- my $pgdata = $self->data_dir;
- my $logfile = $self->logfile;
- my $name = $self->name;
-
- local %ENV = $self->_get_env(PGAPPNAME => undef);
-
- print "### Restarting node \"$name\"\n";
-
- # -w is now the default but having it here does no harm and helps
- # compatibility with older versions.
- TestLib::system_or_bail('pg_ctl', '-w', '-D', $pgdata, '-l', $logfile,
- 'restart');
-
- $self->_update_pid(1);
- return;
-}
-
-=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;
-
- local %ENV = $self->_get_env();
-
- print "### Promoting node \"$name\"\n";
- TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-l', $logfile,
- 'promote');
- return;
-}
-
-=pod
-
-=item $node->logrotate()
-
-Wrapper for pg_ctl logrotate
-
-=cut
-
-sub logrotate
-{
- my ($self) = @_;
- my $port = $self->port;
- my $pgdata = $self->data_dir;
- my $logfile = $self->logfile;
- my $name = $self->name;
-
- local %ENV = $self->_get_env();
-
- print "### Rotating log in node \"$name\"\n";
- TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-l', $logfile,
- 'logrotate');
- return;
-}
-
-# 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(
- 'postgresql.conf', qq(
-primary_conninfo='$root_connstr'
-));
- $self->set_standby_mode();
- return;
-}
-
-# Internal routine to enable archive recovery command on a standby node
-sub enable_restoring
-{
- my ($self, $root_node, $standby) = @_;
- 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(
- 'postgresql.conf', qq(
-restore_command = '$copy_command'
-));
- if ($standby)
- {
- $self->set_standby_mode();
- }
- else
- {
- $self->set_recovery_mode();
- }
- return;
-}
-
-=pod
-
-=item $node->set_recovery_mode()
-
-Place recovery.signal file.
-
-=cut
-
-sub set_recovery_mode
-{
- my ($self) = @_;
-
- $self->append_conf('recovery.signal', '');
- return;
-}
-
-=pod
-
-=item $node->set_standby_mode()
-
-Place standby.signal file.
-
-=cut
-
-sub set_standby_mode
-{
- my ($self) = @_;
-
- $self->append_conf('standby.signal', '');
- return;
-}
-
-# 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'
-));
- return;
-}
-
-# 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;
- return;
-}
-
-=pod
-
-=item PostgresNode->new(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.
-
-=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.
-
-=item install_path => '/path/to/postgres/installation'
-
-Using this parameter is it possible to have nodes pointing to different
-installations, for testing different versions together or the same version
-with different build parameters. The provided path must be the parent of the
-installation's 'bin' and 'lib' directories. In the common case where this is
-not provided, Postgres binaries will be found in the caller's PATH.
-
-=back
-
-=cut
-
-sub new
-{
- my $class = shift;
- my ($name, %params) = @_;
-
- # Select a port.
- my $port;
- if (defined $params{port})
- {
- $port = $params{port};
- }
- else
- {
- # When selecting a port, we look for an unassigned TCP port number,
- # even if we intend to use only Unix-domain sockets. This is clearly
- # necessary on $use_tcp (Windows) configurations, and it seems like a
- # good idea on Unixen as well.
- $port = get_free_port();
- }
-
- # 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;
- }
- }
-
- my $testname = basename($0);
- $testname =~ s/\.[^.]+$//;
- my $node = {
- _port => $port,
- _host => $host,
- _basedir => "$TestLib::tmp_check/t_${testname}_${name}_data",
- _name => $name,
- _logfile_generation => 0,
- _logfile_base => "$TestLib::log_path/${testname}_${name}",
- _logfile => "$TestLib::log_path/${testname}_${name}.log"
- };
-
- if ($params{install_path})
- {
- $node->{_install_path} = $params{install_path};
- }
-
- bless $node, $class;
- mkdir $node->{_basedir}
- or
- BAIL_OUT("could not create data directory \"$node->{_basedir}\": $!");
-
- $node->dump_info;
-
- # Add node to list of nodes
- push(@all_nodes, $node);
-
- $node->_set_pg_version;
-
- my $v = $node->{_pg_version};
-
- carp("PostgresNode isn't fully compatible with version " . $v)
- if $v < 12;
-
- return $node;
-}
-
-# Private routine to run the pg_config binary found in our environment (or in
-# our install_path, if we have one), and set the version from it
-#
-sub _set_pg_version
-{
- my ($self) = @_;
- my $inst = $self->{_install_path};
- my $pg_config = "pg_config";
-
- if (defined $inst)
- {
- # If the _install_path is invalid, our PATH variables might find an
- # unrelated pg_config executable elsewhere. Sanity check the
- # directory.
- BAIL_OUT("directory not found: $inst")
- unless -d $inst;
-
- # If the directory exists but is not the root of a postgresql
- # installation, or if the user configured using
- # --bindir=$SOMEWHERE_ELSE, we're not going to find pg_config, so
- # complain about that, too.
- $pg_config = "$inst/bin/pg_config";
- BAIL_OUT("pg_config not found: $pg_config")
- unless -e $pg_config
- or ($TestLib::windows_os and -e "$pg_config.exe");
- BAIL_OUT("pg_config not executable: $pg_config")
- unless $TestLib::windows_os or -x $pg_config;
-
- # Leave $pg_config install_path qualified, to be sure we get the right
- # version information, below, or die trying
- }
-
- local %ENV = $self->_get_env();
-
- # We only want the version field
- my $version_line = qx{$pg_config --version};
- BAIL_OUT("$pg_config failed: $!") if $?;
-
- $self->{_pg_version} = PostgresVersion->new($version_line);
-
- BAIL_OUT("could not parse pg_config --version output: $version_line")
- unless defined $self->{_pg_version};
-}
-
-# Private routine to return a copy of the environment with the PATH and
-# (DY)LD_LIBRARY_PATH correctly set when there is an install path set for
-# the node.
-#
-# Routines that call Postgres binaries need to call this routine like this:
-#
-# local %ENV = $self->_get_env{[%extra_settings]);
-#
-# A copy of the environment is taken and node's host and port settings are
-# added as PGHOST and PGPORT, Then the extra settings (if any) are applied.
-# Any setting in %extra_settings with a value that is undefined is deleted
-# the remainder are# set. Then the PATH and (DY)LD_LIBRARY_PATH are adjusted
-# if the node's install path is set, and the copy environment is returned.
-#
-# The install path set in new() needs to be a directory containing
-# bin and lib subdirectories as in a standard PostgreSQL installation, so this
-# can't be used with installations where the bin and lib directories don't have
-# a common parent directory.
-sub _get_env
-{
- my $self = shift;
- my %inst_env = (%ENV, PGHOST => $self->{_host}, PGPORT => $self->{_port});
- # the remaining arguments are modifications to make to the environment
- my %mods = (@_);
- while (my ($k, $v) = each %mods)
- {
- if (defined $v)
- {
- $inst_env{$k} = "$v";
- }
- else
- {
- delete $inst_env{$k};
- }
- }
- # now fix up the new environment for the install path
- my $inst = $self->{_install_path};
- if ($inst)
- {
- if ($TestLib::windows_os)
- {
- # Windows picks up DLLs from the PATH rather than *LD_LIBRARY_PATH
- # choose the right path separator
- if ($Config{osname} eq 'MSWin32')
- {
- $inst_env{PATH} = "$inst/bin;$inst/lib;$ENV{PATH}";
- }
- else
- {
- $inst_env{PATH} = "$inst/bin:$inst/lib:$ENV{PATH}";
- }
- }
- else
- {
- my $dylib_name =
- $Config{osname} eq 'darwin'
- ? "DYLD_LIBRARY_PATH"
- : "LD_LIBRARY_PATH";
- $inst_env{PATH} = "$inst/bin:$ENV{PATH}";
- if (exists $ENV{$dylib_name})
- {
- $inst_env{$dylib_name} = "$inst/lib:$ENV{$dylib_name}";
- }
- else
- {
- $inst_env{$dylib_name} = "$inst/lib";
- }
- }
- }
- return (%inst_env);
-}
-
-# Private routine to get an installation path qualified command.
-#
-# IPC::Run maintains a cache, %cmd_cache, mapping commands to paths. Tests
-# which use nodes spanning more than one postgres installation path need to
-# avoid confusing which installation's binaries get run. Setting $ENV{PATH} is
-# insufficient, as IPC::Run does not check to see if the path has changed since
-# caching a command.
-sub installed_command
-{
- my ($self, $cmd) = @_;
-
- # Nodes using alternate installation locations use their installation's
- # bin/ directory explicitly
- return join('/', $self->{_install_path}, 'bin', $cmd)
- if defined $self->{_install_path};
-
- # Nodes implicitly using the default installation location rely on IPC::Run
- # to find the right binary, which should not cause %cmd_cache confusion,
- # because no nodes with other installation paths do it that way.
- return $cmd;
-}
-
-=pod
-
-=item get_free_port()
-
-Locate an unprivileged (high) TCP port that's not currently bound to
-anything. This is used by C<new()>, and also by some test cases that need to
-start other, non-Postgres servers.
-
-Ports assigned to existing PostgresNode objects are automatically
-excluded, even if those servers are not currently running.
-
-XXX A port available now may become unavailable by the time we start
-the desired service.
-
-Note: this is not an instance method. As it's not exported it should be
-called from outside the module as C<PostgresNode::get_free_port()>.
-
-=cut
-
-sub get_free_port
-{
- my $found = 0;
- my $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.
- # Seek a port available for all possible listen_addresses values,
- # so callers can harness this port for the widest range of purposes.
- # The 0.0.0.0 test achieves that for MSYS, which automatically sets
- # SO_EXCLUSIVEADDRUSE. Testing 0.0.0.0 is insufficient for Windows
- # native Perl (https://stackoverflow.com/a/14388707), so we also
- # have to test individual addresses. Doing that for 127.0.0/24
- # addresses other than 127.0.0.1 might fail with EADDRNOTAVAIL on
- # non-Linux, non-Windows kernels.
- #
- # Thus, 0.0.0.0 and individual 127.0.0/24 addresses are tested
- # only on Windows and only when TCP usage is requested.
- if ($found == 1)
- {
- foreach my $addr (qw(127.0.0.1),
- ($use_tcp && $TestLib::windows_os)
- ? qw(127.0.0.2 127.0.0.3 0.0.0.0)
- : ())
- {
- if (!can_bind($addr, $port))
- {
- $found = 0;
- last;
- }
- }
- }
- }
-
- print "# Found port $port\n";
-
- # Update port for next time
- $last_port_assigned = $port;
-
- return $port;
-}
-
-# 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 (in the same order the nodes
-# were created in) when the test script exits.
-END
-{
-
- # take care not to change the script's exit value
- my $exit_code = $?;
-
- foreach my $node (@all_nodes)
- {
- $node->teardown_node;
-
- # skip clean if we are requested to retain the basedir
- next if defined $ENV{'PG_TEST_NOCLEAN'};
-
- # clean basedir on clean test invocation
- $node->clean_node if $exit_code == 0 && TestLib::all_tests_passing();
- }
-
- $? = $exit_code;
-}
-
-=pod
-
-=item $node->teardown_node()
-
-Do an immediate stop of the node
-
-=cut
-
-sub teardown_node
-{
- my $self = shift;
-
- $self->stop('immediate');
- return;
-}
-
-=pod
-
-=item $node->clean_node()
-
-Remove the base directory of the node if the node has been stopped.
-
-=cut
-
-sub clean_node
-{
- my $self = shift;
-
- rmtree $self->{_basedir} unless defined $self->{_pid};
- return;
-}
-
-=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) = @_;
-
- local %ENV = $self->_get_env();
-
- 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";
- }
-
- 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 3 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 connstr => B<value>
-
-If set, use this as the connection string for the connection to the
-backend.
-
-=item replication => B<value>
-
-If set, add B<replication=value> to the conninfo string.
-Passing the literal value C<database> results in a logical replication
-connection.
-
-=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(600)',
- stdout => \$stdout, stderr => \$stderr,
- timeout => 180, 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) = @_;
-
- local %ENV = $self->_get_env();
-
- my $stdout = $params{stdout};
- my $stderr = $params{stderr};
- my $replication = $params{replication};
- my $timeout = undef;
- my $timeout_exception = 'psql timed out';
-
- # Build the connection string.
- my $psql_connstr;
- if (defined $params{connstr})
- {
- $psql_connstr = $params{connstr};
- }
- else
- {
- $psql_connstr = $self->connstr($dbname);
- }
- $psql_connstr .= defined $replication ? " replication=$replication" : "";
-
- my @psql_params = (
- $self->installed_command('psql'),
- '-XAtq', '-d', $psql_connstr, '-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
- # https://metacpan.org/pod/release/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'";
- }
- }
- };
-
- # Note: on Windows, IPC::Run seems to convert \r\n to \n in program output
- # if we're using native Perl, but not if we're using MSys Perl. So do it
- # by hand in the latter case, here and elsewhere.
-
- if (defined $$stdout)
- {
- $$stdout =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
- chomp $$stdout;
- }
-
- if (defined $$stderr)
- {
- $$stderr =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
- chomp $$stderr;
- }
-
- # 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' with sql '$sql'"
- if $ret == 3;
- die "psql returns $ret: '$$stderr'\nwhile running '@psql_params'";
- }
-
- if (wantarray)
- {
- return ($ret, $$stdout, $$stderr);
- }
- else
- {
- return $ret;
- }
-}
-
-=pod
-
-=item $node->background_psql($dbname, \$stdin, \$stdout, $timer, %params) => harness
-
-Invoke B<psql> on B<$dbname> and return an IPC::Run harness object, which the
-caller may use to send input to B<psql>. The process's stdin is sourced from
-the $stdin scalar reference, and its stdout and stderr go to the $stdout
-scalar reference. This allows the caller to act on other parts of the system
-while idling this backend.
-
-The specified timer object is attached to the harness, as well. It's caller's
-responsibility to select the timeout length, and to restart the timer after
-each command if the timeout is per-command.
-
-psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
-disabled. That may be overridden by passing extra psql parameters.
-
-Dies on failure to invoke psql, or if psql fails to connect. Errors occurring
-later are the caller's problem. psql runs with on_error_stop by default so
-that it will stop running sql and return 3 if passed SQL results in an error.
-
-Be sure to "finish" the harness when done with it.
-
-=over
-
-=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 3 is
-returned. Set B<on_error_stop> to 0 to ignore errors instead.
-
-=item replication => B<value>
-
-If set, add B<replication=value> to the conninfo string.
-Passing the literal value C<database> results in a logical replication
-connection.
-
-=item extra_params => ['--single-transaction']
-
-If given, it must be an array reference containing additional parameters to B<psql>.
-
-=back
-
-=cut
-
-sub background_psql
-{
- my ($self, $dbname, $stdin, $stdout, $timer, %params) = @_;
-
- local %ENV = $self->_get_env();
-
- my $replication = $params{replication};
-
- my @psql_params = (
- $self->installed_command('psql'),
- '-XAtq',
- '-d',
- $self->connstr($dbname)
- . (defined $replication ? " replication=$replication" : ""),
- '-f',
- '-');
-
- $params{on_error_stop} = 1 unless defined $params{on_error_stop};
-
- push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
- push @psql_params, @{ $params{extra_params} }
- if defined $params{extra_params};
-
- # Ensure there is no data waiting to be sent:
- $$stdin = "" if ref($stdin);
- # IPC::Run would otherwise append to existing contents:
- $$stdout = "" if ref($stdout);
-
- my $harness = IPC::Run::start \@psql_params,
- '<', $stdin, '>', $stdout, $timer;
-
- # Request some output, and pump until we see it. This means that psql
- # connection failures are caught here, relieving callers of the need to
- # handle those. (Right now, we have no particularly good handling for
- # errors anyway, but that might be added later.)
- my $banner = "background_psql: ready";
- $$stdin = "\\echo $banner\n";
- pump $harness until $$stdout =~ /$banner/ || $timer->is_expired;
-
- die "psql startup timed out" if $timer->is_expired;
-
- return $harness;
-}
-
-=pod
-
-=item $node->interactive_psql($dbname, \$stdin, \$stdout, $timer, %params) => harness
-
-Invoke B<psql> on B<$dbname> and return an IPC::Run harness object,
-which the caller may use to send interactive input to B<psql>.
-The process's stdin is sourced from the $stdin scalar reference,
-and its stdout and stderr go to the $stdout scalar reference.
-ptys are used so that psql thinks it's being called interactively.
-
-The specified timer object is attached to the harness, as well.
-It's caller's responsibility to select the timeout length, and to
-restart the timer after each command if the timeout is per-command.
-
-psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
-disabled. That may be overridden by passing extra psql parameters.
-
-Dies on failure to invoke psql, or if psql fails to connect.
-Errors occurring later are the caller's problem.
-
-Be sure to "finish" the harness when done with it.
-
-The only extra parameter currently accepted is
-
-=over
-
-=item extra_params => ['--single-transaction']
-
-If given, it must be an array reference containing additional parameters to B<psql>.
-
-=back
-
-This requires IO::Pty in addition to IPC::Run.
-
-=cut
-
-sub interactive_psql
-{
- my ($self, $dbname, $stdin, $stdout, $timer, %params) = @_;
-
- local %ENV = $self->_get_env();
-
- my @psql_params = (
- $self->installed_command('psql'),
- '-XAt', '-d', $self->connstr($dbname));
-
- push @psql_params, @{ $params{extra_params} }
- if defined $params{extra_params};
-
- # Ensure there is no data waiting to be sent:
- $$stdin = "" if ref($stdin);
- # IPC::Run would otherwise append to existing contents:
- $$stdout = "" if ref($stdout);
-
- my $harness = IPC::Run::start \@psql_params,
- '<pty<', $stdin, '>pty>', $stdout, $timer;
-
- # Pump until we see psql's help banner. This ensures that callers
- # won't write anything to the pty before it's ready, avoiding an
- # implementation issue in IPC::Run. Also, it means that psql
- # connection failures are caught here, relieving callers of
- # the need to handle those. (Right now, we have no particularly
- # good handling for errors anyway, but that might be added later.)
- pump $harness
- until $$stdout =~ /Type "help" for help/ || $timer->is_expired;
-
- die "psql startup timed out" if $timer->is_expired;
-
- return $harness;
-}
-
-# Common sub of pgbench-invoking interfaces. Makes any requested script files
-# and returns pgbench command-line options causing use of those files.
-sub _pgbench_make_files
-{
- my ($self, $files) = @_;
- my @file_opts;
-
- if (defined $files)
- {
-
- # note: files are ordered for determinism
- for my $fn (sort keys %$files)
- {
- my $filename = $self->basedir . '/' . $fn;
- push @file_opts, '-f', $filename;
-
- # cleanup file weight
- $filename =~ s/\@\d+$//;
-
- #push @filenames, $filename;
- # filenames are expected to be unique on a test
- if (-e $filename)
- {
- ok(0, "$filename must not already exist");
- unlink $filename or die "cannot unlink $filename: $!";
- }
- TestLib::append_to_file($filename, $$files{$fn});
- }
- }
-
- return @file_opts;
-}
-
-=pod
-
-=item $node->pgbench($opts, $stat, $out, $err, $name, $files, @args)
-
-Invoke B<pgbench>, with parameters and files.
-
-=over
-
-=item $opts
-
-Options as a string to be split on spaces.
-
-=item $stat
-
-Expected exit status.
-
-=item $out
-
-Reference to a regexp list that must match stdout.
-
-=item $err
-
-Reference to a regexp list that must match stderr.
-
-=item $name
-
-Name of test for error messages.
-
-=item $files
-
-Reference to filename/contents dictionary.
-
-=item @args
-
-Further raw options or arguments.
-
-=back
-
-=cut
-
-sub pgbench
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my ($self, $opts, $stat, $out, $err, $name, $files, @args) = @_;
- my @cmd = (
- 'pgbench',
- split(/\s+/, $opts),
- $self->_pgbench_make_files($files), @args);
-
- $self->command_checks_all(\@cmd, $stat, $out, $err, $name);
-}
-
-=pod
-
-=item $node->background_pgbench($opts, $files, \$stdout, $timer) => harness
-
-Invoke B<pgbench> and return an IPC::Run harness object. The process's stdin
-is empty, and its stdout and stderr go to the $stdout scalar reference. This
-allows the caller to act on other parts of the system while B<pgbench> is
-running. Errors from B<pgbench> are the caller's problem.
-
-The specified timer object is attached to the harness, as well. It's caller's
-responsibility to select the timeout length, and to restart the timer after
-each command if the timeout is per-command.
-
-Be sure to "finish" the harness when done with it.
-
-=over
-
-=item $opts
-
-Options as a string to be split on spaces.
-
-=item $files
-
-Reference to filename/contents dictionary.
-
-=back
-
-=cut
-
-sub background_pgbench
-{
- my ($self, $opts, $files, $stdout, $timer) = @_;
-
- my @cmd =
- ('pgbench', split(/\s+/, $opts), $self->_pgbench_make_files($files));
-
- local %ENV = $self->_get_env();
-
- my $stdin = "";
- # IPC::Run would otherwise append to existing contents:
- $$stdout = "" if ref($stdout);
-
- my $harness = IPC::Run::start \@cmd, '<', \$stdin, '>', $stdout, '2>&1',
- $timer;
-
- return $harness;
-}
-
-=pod
-
-=item $node->connect_ok($connstr, $test_name, %params)
-
-Attempt a connection with a custom connection string. This is expected
-to succeed.
-
-=over
-
-=item sql => B<value>
-
-If this parameter is set, this query is used for the connection attempt
-instead of the default.
-
-=item expected_stdout => B<value>
-
-If this regular expression is set, matches it with the output generated.
-
-=item log_like => [ qr/required message/ ]
-
-If given, it must be an array reference containing a list of regular
-expressions that must match against the server log, using
-C<Test::More::like()>.
-
-=item log_unlike => [ qr/prohibited message/ ]
-
-If given, it must be an array reference containing a list of regular
-expressions that must NOT match against the server log. They will be
-passed to C<Test::More::unlike()>.
-
-=back
-
-=cut
-
-sub connect_ok
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
- my ($self, $connstr, $test_name, %params) = @_;
-
- my $sql;
- if (defined($params{sql}))
- {
- $sql = $params{sql};
- }
- else
- {
- $sql = "SELECT \$\$connected with $connstr\$\$";
- }
-
- my (@log_like, @log_unlike);
- if (defined($params{log_like}))
- {
- @log_like = @{ $params{log_like} };
- }
- if (defined($params{log_unlike}))
- {
- @log_unlike = @{ $params{log_unlike} };
- }
-
- my $log_location = -s $self->logfile;
-
- # Never prompt for a password, any callers of this routine should
- # have set up things properly, and this should not block.
- my ($ret, $stdout, $stderr) = $self->psql(
- 'postgres',
- $sql,
- extra_params => ['-w'],
- connstr => "$connstr",
- on_error_stop => 0);
-
- is($ret, 0, $test_name);
-
- if (defined($params{expected_stdout}))
- {
- like($stdout, $params{expected_stdout}, "$test_name: matches");
- }
- if (@log_like or @log_unlike)
- {
- my $log_contents = TestLib::slurp_file($self->logfile, $log_location);
-
- while (my $regex = shift @log_like)
- {
- like($log_contents, $regex, "$test_name: log matches");
- }
- while (my $regex = shift @log_unlike)
- {
- unlike($log_contents, $regex, "$test_name: log does not match");
- }
- }
-}
-
-=pod
-
-=item $node->connect_fails($connstr, $test_name, %params)
-
-Attempt a connection with a custom connection string. This is expected
-to fail.
-
-=over
-
-=item expected_stderr => B<value>
-
-If this regular expression is set, matches it with the output generated.
-
-=item log_like => [ qr/required message/ ]
-
-=item log_unlike => [ qr/prohibited message/ ]
-
-See C<connect_ok(...)>, above.
-
-=back
-
-=cut
-
-sub connect_fails
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
- my ($self, $connstr, $test_name, %params) = @_;
-
- my (@log_like, @log_unlike);
- if (defined($params{log_like}))
- {
- @log_like = @{ $params{log_like} };
- }
- if (defined($params{log_unlike}))
- {
- @log_unlike = @{ $params{log_unlike} };
- }
-
- my $log_location = -s $self->logfile;
-
- # Never prompt for a password, any callers of this routine should
- # have set up things properly, and this should not block.
- my ($ret, $stdout, $stderr) = $self->psql(
- 'postgres',
- undef,
- extra_params => ['-w'],
- connstr => "$connstr");
-
- isnt($ret, 0, $test_name);
-
- if (defined($params{expected_stderr}))
- {
- like($stderr, $params{expected_stderr}, "$test_name: matches");
- }
-
- if (@log_like or @log_unlike)
- {
- my $log_contents = TestLib::slurp_file($self->logfile, $log_location);
-
- while (my $regex = shift @log_like)
- {
- like($log_contents, $regex, "$test_name: log matches");
- }
- while (my $regex = shift @log_unlike)
- {
- unlike($log_contents, $regex, "$test_name: log does not match");
- }
- }
-}
-
-=pod
-
-=item $node->poll_query_until($dbname, $query [, $expected ])
-
-Run B<$query> repeatedly, until it returns the B<$expected> result
-('t', or SQL boolean true, by default).
-Continues polling if B<psql> returns an error result.
-Times out after 180 seconds.
-Returns 1 if successful, 0 if timed out.
-
-=cut
-
-sub poll_query_until
-{
- my ($self, $dbname, $query, $expected) = @_;
-
- local %ENV = $self->_get_env();
-
- $expected = 't' unless defined($expected); # default value
-
- my $cmd = [
- $self->installed_command('psql'), '-XAt',
- '-d', $self->connstr($dbname)
- ];
- my ($stdout, $stderr);
- my $max_attempts = 180 * 10;
- my $attempts = 0;
-
- while ($attempts < $max_attempts)
- {
- my $result = IPC::Run::run $cmd, '<', \$query,
- '>', \$stdout, '2>', \$stderr;
-
- $stdout =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
- chomp($stdout);
- $stderr =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
- chomp($stderr);
-
- if ($stdout eq $expected && $stderr eq '')
- {
- return 1;
- }
-
- # Wait 0.1 second before retrying.
- usleep(100_000);
-
- $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.
- diag qq(poll_query_until timed out executing this query:
-$query
-expecting this output:
-$expected
-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
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::command_ok(@_);
- return;
-}
-
-=pod
-
-=item $node->command_fails(...)
-
-TestLib::command_fails with our connection parameters. See command_ok(...)
-
-=cut
-
-sub command_fails
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::command_fails(@_);
- return;
-}
-
-=pod
-
-=item $node->command_like(...)
-
-TestLib::command_like with our connection parameters. See command_ok(...)
-
-=cut
-
-sub command_like
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::command_like(@_);
- return;
-}
-
-=pod
-
-=item $node->command_fails_like(...)
-
-TestLib::command_fails_like with our connection parameters. See command_ok(...)
-
-=cut
-
-sub command_fails_like
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::command_fails_like(@_);
- return;
-}
-
-=pod
-
-=item $node->command_checks_all(...)
-
-TestLib::command_checks_all with our connection parameters. See
-command_ok(...)
-
-=cut
-
-sub command_checks_all
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::command_checks_all(@_);
- return;
-}
-
-=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.
-
-=cut
-
-sub issues_sql_like
-{
- local $Test::Builder::Level = $Test::Builder::Level + 1;
-
- my ($self, $cmd, $expected_sql, $test_name) = @_;
-
- local %ENV = $self->_get_env();
-
- my $log_location = -s $self->logfile;
-
- my $result = TestLib::run_log($cmd);
- ok($result, "@$cmd exit code 0");
- my $log = TestLib::slurp_file($self->logfile, $log_location);
- like($log, $expected_sql, "$test_name: SQL found in server log");
- return;
-}
-
-=pod
-
-=item $node->run_log(...)
-
-Runs a shell command like TestLib::run_log, but with connection parameters set
-so that the command will default to connecting to this PostgresNode.
-
-=cut
-
-sub run_log
-{
- my $self = shift;
-
- local %ENV = $self->_get_env();
-
- TestLib::run_log(@_);
- return;
-}
-
-=pod
-
-=item $node->lsn(mode)
-
-Look up WAL locations on the server:
-
- * insert location (primary only, error on replica)
- * write location (primary only, error on replica)
- * flush location (primary only, error on replica)
- * receive location (always undef on primary)
- * replay location (always undef on primary)
-
-mode must be specified.
-
-=cut
-
-sub lsn
-{
- my ($self, $mode) = @_;
- my %modes = (
- 'insert' => 'pg_current_wal_insert_lsn()',
- 'flush' => 'pg_current_wal_flush_lsn()',
- 'write' => 'pg_current_wal_lsn()',
- 'receive' => 'pg_last_wal_receive_lsn()',
- 'replay' => 'pg_last_wal_replay_lsn()');
-
- $mode = '<undef>' if !defined($mode);
- croak "unknown mode for 'lsn': '$mode', valid modes are "
- . join(', ', keys %modes)
- if !defined($modes{$mode});
-
- my $result = $self->safe_psql('postgres', "SELECT $modes{$mode}");
- chomp($result);
- if ($result eq '')
- {
- return;
- }
- else
- {
- return $result;
- }
-}
-
-=pod
-
-=item $node->wait_for_catchup(standby_name, mode, target_lsn)
-
-Wait for the node with application_name standby_name (usually from node->name,
-also works for logical subscriptions)
-until its replication location in pg_stat_replication equals or passes the
-upstream's WAL insert point at the time this function is called. By default
-the replay_lsn is waited for, but 'mode' may be specified to wait for any of
-sent|write|flush|replay. The connection catching up must be in a streaming
-state.
-
-If there is no active replication connection from this peer, waits until
-poll_query_until timeout.
-
-Requires that the 'postgres' db exists and is accessible.
-
-target_lsn may be any arbitrary lsn, but is typically $primary_node->lsn('insert').
-If omitted, pg_current_wal_lsn() is used.
-
-This is not a test. It die()s on failure.
-
-=cut
-
-sub wait_for_catchup
-{
- my ($self, $standby_name, $mode, $target_lsn) = @_;
- $mode = defined($mode) ? $mode : 'replay';
- my %valid_modes =
- ('sent' => 1, 'write' => 1, 'flush' => 1, 'replay' => 1);
- croak "unknown mode $mode for 'wait_for_catchup', valid modes are "
- . join(', ', keys(%valid_modes))
- unless exists($valid_modes{$mode});
-
- # Allow passing of a PostgresNode instance as shorthand
- if (blessed($standby_name) && $standby_name->isa("PostgresNode"))
- {
- $standby_name = $standby_name->name;
- }
- my $lsn_expr;
- if (defined($target_lsn))
- {
- $lsn_expr = "'$target_lsn'";
- }
- else
- {
- $lsn_expr = 'pg_current_wal_lsn()';
- }
- print "Waiting for replication conn "
- . $standby_name . "'s "
- . $mode
- . "_lsn to pass "
- . $lsn_expr . " on "
- . $self->name . "\n";
- my $query =
- qq[SELECT $lsn_expr <= ${mode}_lsn AND state = 'streaming' FROM pg_catalog.pg_stat_replication WHERE application_name = '$standby_name';];
- $self->poll_query_until('postgres', $query)
- or croak "timed out waiting for catchup";
- print "done\n";
- return;
-}
-
-=pod
-
-=item $node->wait_for_slot_catchup(slot_name, mode, target_lsn)
-
-Wait for the named replication slot to equal or pass the supplied target_lsn.
-The location used is the restart_lsn unless mode is given, in which case it may
-be 'restart' or 'confirmed_flush'.
-
-Requires that the 'postgres' db exists and is accessible.
-
-This is not a test. It die()s on failure.
-
-If the slot is not active, will time out after poll_query_until's timeout.
-
-target_lsn may be any arbitrary lsn, but is typically $primary_node->lsn('insert').
-
-Note that for logical slots, restart_lsn is held down by the oldest in-progress tx.
-
-=cut
-
-sub wait_for_slot_catchup
-{
- my ($self, $slot_name, $mode, $target_lsn) = @_;
- $mode = defined($mode) ? $mode : 'restart';
- if (!($mode eq 'restart' || $mode eq 'confirmed_flush'))
- {
- croak "valid modes are restart, confirmed_flush";
- }
- croak 'target lsn must be specified' unless defined($target_lsn);
- print "Waiting for replication slot "
- . $slot_name . "'s "
- . $mode
- . "_lsn to pass "
- . $target_lsn . " on "
- . $self->name . "\n";
- my $query =
- qq[SELECT '$target_lsn' <= ${mode}_lsn FROM pg_catalog.pg_replication_slots WHERE slot_name = '$slot_name';];
- $self->poll_query_until('postgres', $query)
- or croak "timed out waiting for catchup";
- print "done\n";
- return;
-}
-
-=pod
-
-=item $node->query_hash($dbname, $query, @columns)
-
-Execute $query on $dbname, replacing any appearance of the string __COLUMNS__
-within the query with a comma-separated list of @columns.
-
-If __COLUMNS__ does not appear in the query, its result columns must EXACTLY
-match the order and number (but not necessarily alias) of supplied @columns.
-
-The query must return zero or one rows.
-
-Return a hash-ref representation of the results of the query, with any empty
-or null results as defined keys with an empty-string value. There is no way
-to differentiate between null and empty-string result fields.
-
-If the query returns zero rows, return a hash with all columns empty. There
-is no way to differentiate between zero rows returned and a row with only
-null columns.
-
-=cut
-
-sub query_hash
-{
- my ($self, $dbname, $query, @columns) = @_;
- croak 'calls in array context for multi-row results not supported yet'
- if (wantarray);
-
- # Replace __COLUMNS__ if found
- substr($query, index($query, '__COLUMNS__'), length('__COLUMNS__')) =
- join(', ', @columns)
- if index($query, '__COLUMNS__') >= 0;
- my $result = $self->safe_psql($dbname, $query);
-
- # hash slice, see http://stackoverflow.com/a/16755894/398670 .
- #
- # Fills the hash with empty strings produced by x-operator element
- # duplication if result is an empty row
- #
- my %val;
- @val{@columns} =
- $result ne '' ? split(qr/\|/, $result, -1) : ('',) x scalar(@columns);
- return \%val;
-}
-
-=pod
-
-=item $node->slot(slot_name)
-
-Return hash-ref of replication slot data for the named slot, or a hash-ref with
-all values '' if not found. Does not differentiate between null and empty string
-for fields, no field is ever undef.
-
-The restart_lsn and confirmed_flush_lsn fields are returned verbatim, and also
-as a 2-list of [highword, lowword] integer. Since we rely on Perl 5.8.8 we can't
-"use bigint", it's from 5.20, and we can't assume we have Math::Bigint from CPAN
-either.
-
-=cut
-
-sub slot
-{
- my ($self, $slot_name) = @_;
- my @columns = (
- 'plugin', 'slot_type', 'datoid', 'database',
- 'active', 'active_pid', 'xmin', 'catalog_xmin',
- 'restart_lsn');
- return $self->query_hash(
- 'postgres',
- "SELECT __COLUMNS__ FROM pg_catalog.pg_replication_slots WHERE slot_name = '$slot_name'",
- @columns);
-}
-
-=pod
-
-=item $node->pg_recvlogical_upto(self, dbname, slot_name, endpos, timeout_secs, ...)
-
-Invoke pg_recvlogical to read from slot_name on dbname until LSN endpos, which
-corresponds to pg_recvlogical --endpos. Gives up after timeout (if nonzero).
-
-Disallows pg_recvlogical from internally retrying on error by passing --no-loop.
-
-Plugin options are passed as additional keyword arguments.
-
-If called in scalar context, returns stdout, and die()s on timeout or nonzero return.
-
-If called in array context, returns a tuple of (retval, stdout, stderr, timeout).
-timeout is the IPC::Run::Timeout object whose is_expired method can be tested
-to check for timeout. retval is undef on timeout.
-
-=cut
-
-sub pg_recvlogical_upto
-{
- my ($self, $dbname, $slot_name, $endpos, $timeout_secs, %plugin_options)
- = @_;
-
- local %ENV = $self->_get_env();
-
- my ($stdout, $stderr);
-
- my $timeout_exception = 'pg_recvlogical timed out';
-
- croak 'slot name must be specified' unless defined($slot_name);
- croak 'endpos must be specified' unless defined($endpos);
-
- my @cmd = (
- $self->installed_command('pg_recvlogical'),
- '-S', $slot_name, '--dbname', $self->connstr($dbname));
- push @cmd, '--endpos', $endpos;
- push @cmd, '-f', '-', '--no-loop', '--start';
-
- while (my ($k, $v) = each %plugin_options)
- {
- croak "= is not permitted to appear in replication option name"
- if ($k =~ qr/=/);
- push @cmd, "-o", "$k=$v";
- }
-
- my $timeout;
- $timeout =
- IPC::Run::timeout($timeout_secs, exception => $timeout_exception)
- if $timeout_secs;
- my $ret = 0;
-
- do
- {
- local $@;
- eval {
- IPC::Run::run(\@cmd, ">", \$stdout, "2>", \$stderr, $timeout);
- $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 !~ qr/$timeout_exception/);
-
- $ret = undef;
-
- die "Got timeout exception '$exc_save' but timer not expired?!"
- unless $timeout->is_expired;
-
- die
- "$exc_save waiting for endpos $endpos with stdout '$stdout', stderr '$stderr'"
- unless wantarray;
- }
- };
-
- $stdout =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
- $stderr =~ s/\r\n/\n/g if $Config{osname} eq 'msys';
-
- if (wantarray)
- {
- return ($ret, $stdout, $stderr, $timeout);
- }
- else
- {
- die
- "pg_recvlogical exited with code '$ret', stdout '$stdout' and stderr '$stderr'"
- if $ret;
- return $stdout;
- }
-}
-
-=pod
-
-=back
-
-=cut
-
-1;