-# PostgresNode, class representing a data directory and postmaster.
-#
-# This contains a basic 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.
+=pod
+
+=head1 NAME
+
+PostgresNode - class representing PostgreSQL server instance
+
+=head1 SYNOPSIS
+
+ use PostgresNode;
+
+ my $node = 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('fast');
+
+ # run a query with psql
+ # like: psql -qAXt postgres -c 'SELECT 1;'
+ $psql_stdout = $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('testbackup');
+
+ # 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 envronment set up to
+point to the instance.
+
+The IPC::Run module is required.
+
+=cut
package PostgresNode;
$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 = shift;
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) = @_;
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) = @_;
return "$res/pgdata";
}
+=pod
+
+=item $node->archive_dir()
+
+If archiving is enabled, WAL files go here.
+
+=cut
+
sub archive_dir
{
my ($self) = @_;
return "$basedir/archives";
}
+=pod
+
+=item $node->backup_dir()
+
+The output path for backups taken with $node->backup()
+
+=cut
+
sub backup_dir
{
my ($self) = @_;
return "$basedir/backup";
}
-# Dump node information
+=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 "Name: " . $self->name . "\n";
- print "Data directory: " . $self->data_dir . "\n";
- print "Backup directory: " . $self->backup_dir . "\n";
- print "Archive directory: " . $self->archive_dir . "\n";
- print "Connection string: " . $self->connstr . "\n";
- print "Log file: " . $self->logfile . "\n";
+ 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)
close $hba;
}
-# 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).
+=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.
+
+The new node is set up in a fast but unsafe configuration where fsync is
+disabled.
+
+=cut
+
sub init
{
my ($self, %params) = @_;
$self->set_replication_conf if ($params{hba_permit_replication});
}
+=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 NOT automatically appended to the string.
+
+=cut
+
sub append_conf
{
my ($self, $filename, $str) = @_;
TestLib::append_to_file($conffile, $str);
}
+=pod
+
+=item $node->backup(backup_name)
+
+Create a hot backup with pg_basebackup in $node->backup_dir,
+including the transaction logs. xlogs are fetched at the
+end of the backup, not streamed.
+
+You'll have to configure a suitable max_wal_senders on the
+target server since it isn't done by default.
+
+=cut
+
sub backup
{
my ($self, $backup_name) = @_;
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 init.
+
+A recovery.conf is not created.
+
+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) = @_;
$self->set_replication_conf;
}
+=pod
+
+=item $node->start()
+
+Wrapper for pg_ctl -w start
+
+Start the node and wait until it is ready to accept connections.
+
+=cut
+
sub start
{
my ($self) = @_;
$self->_update_pid;
}
+=pod
+
+=item $node->stop(mode)
+
+Stop the node using pg_ctl -m $mode and wait for it to stop.
+
+=cut
+
sub stop
{
my ($self, $mode) = @_;
$self->_update_pid;
}
+=pod
+
+=item $node->restart()
+
+wrapper for pg_ctl -w restart
+
+=cut
+
sub restart
{
my ($self) = @_;
$self->_update_pid;
}
+
+# Internal method
sub _update_pid
{
my $self = shift;
print "# No postmaster PID\n";
}
-#
-# Cluster management functions
-#
+=pod
+
+=item get_new_node(node_name)
+
+Build a new PostgresNode object, assigning a free port number. Standalone
+function that's automatically imported.
+
+We also register the node, to avoid the port number from being reused
+for another node even when this one is not active.
+
+You should generally use this instead of PostgresNode::new(...).
+
+=cut
-# Build a new PostgresNode object, assigning a free port number.
-#
-# We also register the node, to avoid the port number from being reused
-# for another node even when this one is not active.
sub get_new_node
{
my $name = shift;
return $node;
}
+# Attempt automatic cleanup
sub DESTROY
{
my $self = shift;
TestLib::system_log('pg_ctl', 'kill', 'QUIT', $self->{_pid});
}
+=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->psql(dbname, sql)
+
+Run a query with psql and return stdout, or on error print stderr.
+
+Executes a query/script with psql and returns psql's standard output. psql is
+run in unaligned tuples-only quiet mode with psqlrc disabled so simple queries
+will just return the result row(s) with fields separated by commas.
+
+=cut
+
sub psql
{
my ($self, $dbname, $sql) = @_;
return $stdout;
}
-# Run a query once a second, until it returns 't' (i.e. SQL boolean true).
+=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 90 seconds.
+
+=cut
+
sub poll_query_until
{
my ($self, $dbname, $query) = @_;
return 0;
}
+=pod
+
+=item $node->command_ok(...)
+
+Runs a shell command like TestLib::command_ok, but with PGPORT
+set so that the command will default to connecting to this
+PostgresNode.
+
+=cut
+
sub command_ok
{
my $self = shift;
TestLib::command_ok(@_);
}
+=pod
+
+=item $node->command_fails(...) - TestLib::command_fails with our PGPORT
+
+See command_ok(...)
+
+=cut
+
sub command_fails
{
my $self = shift;
TestLib::command_fails(@_);
}
+=pod
+
+=item $node->command_like(...)
+
+TestLib::command_like with our PGPORT. See command_ok(...)
+
+=cut
+
sub command_like
{
my $self = shift;
TestLib::command_like(@_);
}
-# Run a command on the node, then verify that $expected_sql appears in the
-# server log file.
+=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.
+
+=cut
+
sub issues_sql_like
{
my ($self, $cmd, $expected_sql, $test_name) = @_;
like($log, $expected_sql, "$test_name: SQL found in server log");
}
+=pod
+
+=back
+
+=cut
+
1;