# Reproducer: a hot standby deadlock between the startup process, waiting
# for a lock held by a session, and that session, waiting for a lock that the
# startup process holds, is never detected after an unrelated wakeup of the
# startup process.  With max_standby_streaming_delay = -1, recovery is stuck.
#
# A standby session holds a lock on table1 and waits for a lock on table2
# that the startup process holds for a prepared transaction.  Its own
# deadlock check runs before replay waits for table1, and finds nothing.
# Replay of LOCK TABLE table1 then makes the startup process wait for the
# session.  Now only the startup process can find the deadlock: after
# deadlock_timeout, it asks the session to check again.
#
# Any wakeup that does not end the startup process's wait makes it cancel its
# timers and start a new deadlock timeout.  The kernel alarm for the old one
# stays pending and wakes it again before the new one expires, and so on, so
# the request is never sent.  Here a memory-context request provides the
# first wakeup.
#
# The assertions below describe the deadlock, so they pass on affected
# servers.

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

my $primary = PostgreSQL::Test::Cluster->new('primary');
$primary->init(allows_streaming => 1);
$primary->append_conf('postgresql.conf', 'max_prepared_transactions = 10');
$primary->start;
$primary->backup('backup');

# Disable the startup progress timer, so that the memory-context request is
# the only wakeup.
my $standby = PostgreSQL::Test::Cluster->new('standby');
$standby->init_from_backup($primary, 'backup', has_streaming => 1);
$standby->append_conf(
	'postgresql.conf', q[
max_standby_streaming_delay = -1
deadlock_timeout = 3s
log_recovery_conflict_waits = on
log_startup_progress_interval = 0
]);
$standby->start;

# Make the startup process hold an AccessExclusiveLock on table2.
$primary->safe_psql(
	'postgres', q[
CREATE TABLE table1(a int);
CREATE TABLE table2(a int);
BEGIN;
LOCK TABLE table2;
PREPARE TRANSACTION 'lock';
]);
$primary->wait_for_replay_catchup($standby);

# The session locks table1, then waits for the lock on table2.  Make its own
# deadlock check run quickly, and log that it ran, so that it finishes before
# the startup process waits for table1.
my $session = $standby->background_psql('postgres', on_error_stop => 0);
my $session_pid = $session->query_safe('SELECT pg_backend_pid()');
my $log_offset = -s $standby->logfile;
$session->query_until(
	qr/^1$/m, q[
BEGIN;
SET LOCAL deadlock_timeout = '10ms';
SET LOCAL log_lock_waits = on;
LOCK TABLE table1 IN ACCESS SHARE MODE;
SELECT 1;
SELECT * FROM table2;
]);
$standby->wait_for_log(
	qr/process $session_pid still waiting for AccessShareLock/, $log_offset);

# Replaying the lock on table1 makes the startup process wait for the
# session.
$primary->safe_psql('postgres', 'BEGIN; LOCK TABLE table1; COMMIT;');
$standby->wait_for_event('startup', 'relation');
ok(1, 'startup process waits for the session');

# Wake the startup process before its deadlock timeout expires.
my $startup_pid = $standby->safe_psql('postgres',
	"SELECT pid FROM pg_stat_activity WHERE backend_type = 'startup'");
sleep(1);
$standby->safe_psql('postgres',
	"SELECT pg_log_backend_memory_contexts($startup_pid)");
$standby->wait_for_log(qr/logging memory contexts of PID $startup_pid\b/,
	$log_offset);

# deadlock_timeout passes several times, but the deadlock is not resolved.
sleep(10);

is( $standby->safe_psql(
		'postgres',
		"SELECT wait_event FROM pg_stat_activity WHERE backend_type = 'startup'"
	),
	'relation',
	'startup process is still waiting for the session');
is( $standby->safe_psql(
		'postgres',
		"SELECT wait_event_type || ':' || wait_event FROM pg_stat_activity WHERE pid = $session_pid"
	),
	'Lock:relation',
	'session is still waiting for the startup process');
unlike(
	slurp_file($standby->logfile, $log_offset),
	qr/ERROR:  deadlock detected/,
	'no deadlock was detected');

# Break the cycle by canceling the session.
$standby->safe_psql('postgres', "SELECT pg_cancel_backend($session_pid)");
$primary->wait_for_replay_catchup($standby);
ok(1, 'replay resumes once the session is canceled');

$session->quit;
$primary->safe_psql('postgres', q[ROLLBACK PREPARED 'lock']);

done_testing();
