
# Copyright (c) 2021-2025, PostgreSQL Global Development Group

# Test postmaster start and stop state machine.

use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;

#
# Test that dead-end backends don't prevent the server from shutting
# down.
#
# Dead-end backends can linger until they reach
# authentication_timeout. We use a long authentication_timeout and a
# much shorter timeout for the "pg_ctl stop" operation, to test that
# if dead-end backends are killed at fast shut down. If they're not,
# "pg_ctl stop" will error out before the authentication timeout kicks
# in and cleans up the dead-end backends.
my $authentication_timeout = $PostgreSQL::Test::Utils::timeout_default;

# Don't fail due to hitting the max value allowed for authentication_timeout.
$authentication_timeout = 600 unless $authentication_timeout < 600;

my $stop_timeout = $authentication_timeout / 2;

# Initialize the server with low connection limits, to test dead-end backends
my $node = PostgreSQL::Test::Cluster->new('main');
$node->init;
$node->append_conf('postgresql.conf', "max_connections = 5");
$node->append_conf('postgresql.conf', "max_wal_senders = 0");
$node->append_conf('postgresql.conf', "autovacuum_max_workers = 1");
$node->append_conf('postgresql.conf', "max_worker_processes = 1");
$node->append_conf('postgresql.conf',
	"log_connections = 'receipt,authentication,authorization'");
$node->append_conf('postgresql.conf', "log_min_messages = debug2");
$node->append_conf('postgresql.conf',
	"authentication_timeout = '$authentication_timeout s'");
$node->append_conf('postgresql.conf', 'trace_connection_negotiation=on');
$node->start;

if (!$node->raw_connect_works())
{
	plan skip_all => "this test requires working raw_connect()";
}

# Restart the server to ensure that the backend launched for
# raw_connect_works() is gone. Otherwise, it might free up the
# connection slot later, when we expect all the slots to be in use.
$node->restart;

my @raw_connections = ();

# Open a lot of TCP (or Unix domain socket) connections to use up all
# the connection slots. Beyond a certain number (roughly 2x
# max_connections), they will be "dead-end backends".
for (my $i = 0; $i <= 20; $i++)
{
	my $sock = $node->raw_connect();

	# On a busy system, the server might reject connections if
	# postmaster cannot accept() them fast enough. The exact limit
	# and behavior depends on the platform. To make this reliable,
	# we attempt SSL negotiation on each connection before opening
	# next one. The server will reject the SSL negotiations, but
	# when it does so, we know that the backend has been launched
	# and we should be able to open another connection.

	# SSLRequest packet consists of packet length followed by
	# NEGOTIATE_SSL_CODE.
	my $negotiate_ssl_code = pack("Nnn", 8, 1234, 5679);
	my $sent = $sock->send($negotiate_ssl_code);

	# Read reply. We expect the server to reject it with 'N'
	my $reply = "";
	$sock->recv($reply, 1);
	is($reply, "N", "dead-end connection $i");

	push(@raw_connections, $sock);
}

# When all the connection slots are in use, new connections will fail
# before even looking up the user. Hence you now get "sorry, too many
# clients already" instead of "role does not exist" error. Test that
# to ensure that we have used up all the slots.
$node->connect_fails("dbname=postgres user=invalid_user",
	"connection is rejected when all slots are in use",
	expected_stderr => qr/FATAL:  sorry, too many clients already/);

# Open one more connection, to really ensure that we have at least one
# dead-end backend.
my $sock = $node->raw_connect();

# Test that the dead-end backends don't prevent the server from stopping.
$node->stop('fast', timeout => $stop_timeout);

$node->start();
$node->connect_ok("dbname=postgres", "works after restart");

# Clean up
foreach my $socket (@raw_connections)
{
	$socket->close();
}

done_testing();
