diff options
Diffstat (limited to 'src/test')
-rw-r--r-- | src/test/perl/PostgresNode.pm | 184 | ||||
-rw-r--r-- | src/test/recovery/t/017_shm.pl | 200 |
2 files changed, 347 insertions, 37 deletions
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm index 81deed98430..f6570639e00 100644 --- a/src/test/perl/PostgresNode.pm +++ b/src/test/perl/PostgresNode.pm @@ -104,7 +104,8 @@ our @EXPORT = qw( get_new_node ); -our ($test_localhost, $test_pghost, $last_port_assigned, @all_nodes, $died); +our ($use_tcp, $test_localhost, $test_pghost, $last_host_assigned, + $last_port_assigned, @all_nodes, $died); # Windows path to virtual file system root @@ -118,13 +119,14 @@ if ($Config{osname} eq 'msys') INIT { - # PGHOST is set once and for all through a single series of tests when - # this module is loaded. - $test_localhost = "127.0.0.1"; - $test_pghost = - $TestLib::windows_os ? $test_localhost : TestLib::tempdir_short; - $ENV{PGHOST} = $test_pghost; - $ENV{PGDATABASE} = 'postgres'; + # 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; @@ -155,7 +157,9 @@ sub new _host => $pghost, _basedir => "$TestLib::tmp_check/t_${testname}_${name}_data", _name => $name, - _logfile => "$TestLib::log_path/${testname}_${name}.log" + _logfile_generation => 0, + _logfile_base => "$TestLib::log_path/${testname}_${name}", + _logfile => "$TestLib::log_path/${testname}_${name}.log" }; bless $self, $class; @@ -473,8 +477,9 @@ sub init print $conf "max_wal_senders = 0\n"; } - if ($TestLib::windows_os) + if ($use_tcp) { + print $conf "unix_socket_directories = ''\n"; print $conf "listen_addresses = '$host'\n"; } else @@ -536,12 +541,11 @@ sub backup { my ($self, $backup_name) = @_; my $backup_path = $self->backup_dir . '/' . $backup_name; - my $port = $self->port; my $name = $self->name; print "# Taking pg_basebackup $backup_name from node \"$name\"\n"; - TestLib::system_or_bail('pg_basebackup', '-D', $backup_path, '-p', $port, - '--no-sync'); + TestLib::system_or_bail('pg_basebackup', '-D', $backup_path, '-h', + $self->host, '-p', $self->port, '--no-sync'); print "# Backup finished\n"; return; } @@ -651,6 +655,7 @@ 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; @@ -677,6 +682,15 @@ sub init_from_backup 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) if $params{has_restoring}; return; @@ -684,17 +698,45 @@ port = $port =pod -=item $node->start() +=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) = @_; + my ($self, %params) = @_; my $port = $self->port; my $pgdata = $self->data_dir; my $name = $self->name; @@ -721,10 +763,34 @@ sub start { print "# pg_ctl start failed; logfile:\n"; print TestLib::slurp_file($self->logfile); - BAIL_OUT("pg_ctl start failed"); + 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; } @@ -965,7 +1031,7 @@ sub _update_pid =pod -=item PostgresNode->get_new_node(node_name) +=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 @@ -974,6 +1040,22 @@ 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>. @@ -982,10 +1064,11 @@ which can only create objects of class C<PostgresNode>. sub get_new_node { my $class = 'PostgresNode'; - $class = shift if 1 < scalar @_; - my $name = shift; - my $found = 0; - my $port = $last_port_assigned; + $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) { @@ -1002,13 +1085,15 @@ sub get_new_node $found = 0 if ($node->port == $port); } - # Check to see if anything else is listening on this TCP port. - # This is *necessary* on Windows, and seems like a good idea - # on Unixen as well, even though we don't ask the postmaster - # to open a TCP port on Unix. + # Check to see if anything else is listening on this TCP port. Accept + # only ports available for all possible listen_addresses values, so + # the caller can harness this port for the widest range of purposes. + # This is *necessary* on Windows, and seems like a good idea on Unixen + # as well, even though we don't ask the postmaster to open a TCP port + # on Unix. if ($found == 1) { - my $iaddr = inet_aton($test_localhost); + my $iaddr = inet_aton('0.0.0.0'); my $paddr = sockaddr_in($port, $iaddr); my $proto = getprotobyname("tcp"); @@ -1024,16 +1109,35 @@ sub get_new_node } } - print "# Found free port $port\n"; + print "# Found port $port\n"; + + # Select a host. + my $host = $test_pghost; + if ($params{own_host}) + { + if ($use_tcp) + { + # This assumes $use_tcp platforms treat every address in + # 127.0.0.1/24, not just 127.0.0.1, as a usable loopback. + $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, $test_pghost, $port); + my $node = $class->new($name, $host, $port); # Add node to list of nodes push(@all_nodes, $node); # And update port for next time - $last_port_assigned = $port; + $port_is_forced or $last_port_assigned = $port; return $node; } @@ -1424,9 +1528,8 @@ $stderr); =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. +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 @@ -1436,6 +1539,7 @@ sub command_ok my $self = shift; + local $ENV{PGHOST} = $self->host; local $ENV{PGPORT} = $self->port; TestLib::command_ok(@_); @@ -1446,7 +1550,7 @@ sub command_ok =item $node->command_fails(...) -TestLib::command_fails with our PGPORT. See command_ok(...) +TestLib::command_fails with our connection parameters. See command_ok(...) =cut @@ -1456,6 +1560,7 @@ sub command_fails my $self = shift; + local $ENV{PGHOST} = $self->host; local $ENV{PGPORT} = $self->port; TestLib::command_fails(@_); @@ -1466,7 +1571,7 @@ sub command_fails =item $node->command_like(...) -TestLib::command_like with our PGPORT. See command_ok(...) +TestLib::command_like with our connection parameters. See command_ok(...) =cut @@ -1476,6 +1581,7 @@ sub command_like my $self = shift; + local $ENV{PGHOST} = $self->host; local $ENV{PGPORT} = $self->port; TestLib::command_like(@_); @@ -1486,7 +1592,8 @@ sub command_like =item $node->command_checks_all(...) -TestLib::command_checks_all with our PGPORT. See command_ok(...) +TestLib::command_checks_all with our connection parameters. See +command_ok(...) =cut @@ -1496,6 +1603,7 @@ sub command_checks_all my $self = shift; + local $ENV{PGHOST} = $self->host; local $ENV{PGPORT} = $self->port; TestLib::command_checks_all(@_); @@ -1520,6 +1628,7 @@ 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; @@ -1534,8 +1643,8 @@ sub issues_sql_like =item $node->run_log(...) -Runs a shell command like TestLib::run_log, but with PGPORT set so -that the command will default to connecting to this PostgresNode. +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 @@ -1543,6 +1652,7 @@ sub run_log { my $self = shift; + local $ENV{PGHOST} = $self->host; local $ENV{PGPORT} = $self->port; TestLib::run_log(@_); diff --git a/src/test/recovery/t/017_shm.pl b/src/test/recovery/t/017_shm.pl new file mode 100644 index 00000000000..3cbe938ddd1 --- /dev/null +++ b/src/test/recovery/t/017_shm.pl @@ -0,0 +1,200 @@ +# +# 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); + +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; + $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; + # 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 1 FROM pg_stat_activity WHERE query = '$slow_query'", '1'), + '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; # on Windows, can't open old log for writing +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' ], + '<', \('SELECT 1 + 1'), '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. Windows is immune, because +# its GetSharedMemName() use DataDir strings, not numeric keys. +$flea->stop; # release first key +is( $gnat->start(fail_ok => 1), + $TestLib::windows_os ? 0 : 1, + 'key turnover fools only sysv_shmem.c'); +$gnat->stop; # release first key (no-op on $TestLib::windows_os) +$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; +} |