From 4e2f6788f5f74bdc4cd2ad5e39d0a41f9ecdf758 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Mon, 27 Jul 2026 00:11:59 +0200
Subject: [PATCH v1 11/12] POC: recovery: port TAP test 031_recovery_conflict
 to pytest
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The Perl test checks the standby log for each conflict and then calls
reconnect_and_clear(), so whatever the conflicting session received is thrown
away instead of asserted. In Python asserting it is very simple:

    with pytest.raises(
        LibpqError, match="canceling statement due to conflict with recovery"
    ):
        waiter.result()

Test runtime changes in CI:

    platform    perl             pytest           diff
    ----------- ---------------- ---------------- ----------------
    windows       7.5s (±0.2)      4.4s (±0.2)      -3.0s (-40%)
    mingw         6.9s (±0.2)      4.3s (±0.0)      -2.7s (-38%)
    linux-64      3.9s (±0.0)      3.0s (±0.1)      -0.8s (-22%)
    macos         5.2s (±0.5)      4.4s (±0.4)      -0.8s (-16%)
    linux-32      2.4s (±0.0)      2.1s (±0.1)      -0.3s (-11%)

Timings are means of 5 runs of each form, interleaved on one CI runner with
nothing else running on it; ± is the standard deviation across the 5 runs.

LOC (no comments or blanks, with tokei): 255 -> 187 (-27%).
---
 src/test/recovery/meson.build                 |   2 +-
 .../pyt/test_031_recovery_conflict.py         | 308 ++++++++++++++++
 src/test/recovery/t/031_recovery_conflict.pl  | 333 ------------------
 3 files changed, 309 insertions(+), 334 deletions(-)
 create mode 100644 src/test/recovery/pyt/test_031_recovery_conflict.py
 delete mode 100644 src/test/recovery/t/031_recovery_conflict.pl

diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 955f5e6a316..14f768c4058 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -8,6 +8,7 @@ tests += {
     'test_kwargs': {'priority': 40}, # recovery tests are slow, start early
     'tests': [
       'pyt/test_029_stats_restart.py',
+      'pyt/test_031_recovery_conflict.py',
       'pyt/test_049_wait_for_lsn.py',
     ],
   },
@@ -45,7 +46,6 @@ tests += {
       't/027_stream_regress.pl',
       't/028_pitr_timelines.pl',
       't/030_stats_cleanup_replica.pl',
-      't/031_recovery_conflict.pl',
       't/032_relfilenode_reuse.pl',
       't/033_replay_tsp_drops.pl',
       't/034_create_database.pl',
diff --git a/src/test/recovery/pyt/test_031_recovery_conflict.py b/src/test/recovery/pyt/test_031_recovery_conflict.py
new file mode 100644
index 00000000000..39e35536f35
--- /dev/null
+++ b/src/test/recovery/pyt/test_031_recovery_conflict.py
@@ -0,0 +1,308 @@
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+"""Port of src/test/recovery/t/031_recovery_conflict.pl.
+
+Test that connections to a hot standby are correctly canceled when a recovery
+conflict is detected, and that pg_stat_database_conflicts is populated. Each
+conflict type kills the standby session that triggers it, so a fresh background
+session is opened per scenario (the Perl test reconnects one psql).
+"""
+
+import pytest
+from libpq import LibpqError
+from pypg import pg_test_timeout_default
+
+TABLESPACE1 = "test_recovery_conflict_tblspc"
+TEST_DB = "test_db"
+TABLE1 = "test_recovery_conflict_table1"
+TABLE2 = "test_recovery_conflict_table2"
+CURSOR1 = "test_recovery_conflict_cursor"
+
+
+def test_recovery_conflict(create_pg):
+    primary = create_pg(
+        "primary",
+        allows_streaming=True,
+        conf={
+            "allow_in_place_tablespaces": True,
+            "log_temp_files": 0,
+            # for deadlock test
+            "max_prepared_transactions": 10,
+            # wait some to test the wait paths as well, but not long
+            "max_standby_streaming_delay": "50ms",
+            "temp_tablespaces": TABLESPACE1,
+            # Some recovery-conflict logging is only exercised after
+            # deadlock_timeout; give minimal coverage of that code.
+            "log_recovery_conflict_waits": True,
+            "deadlock_timeout": "10ms",
+        },
+    )
+    primary.sql(f"CREATE TABLESPACE {TABLESPACE1} LOCATION ''")
+
+    backup = primary.backup("my_backup")
+    standby = create_pg("standby", from_backup=backup, streaming_primary=primary)
+
+    # Use a new database to be able to trigger a database recovery conflict.
+    # Nearly everything below runs against it on both nodes, so make it the
+    # default rather than passing dbname= to every call. The final scenario
+    # drops it, and reaches back to postgres explicitly to do so.
+    primary.sql(f"CREATE DATABASE {TEST_DB}")
+    primary.default_connection_options = {"dbname": TEST_DB}
+    standby.default_connection_options = {"dbname": TEST_DB}
+
+    primary.sql_batch_oneshot(
+        f"CREATE TABLE {TABLE1}(a int, b int)",
+        f"INSERT INTO {TABLE1} SELECT i % 3, 0 FROM generate_series(1,20) i",
+        f"CREATE TABLE {TABLE2}(a int, b int)",
+    )
+
+    primary.wait_for_catchup(standby)
+
+    def check_conflict_stat(conflict_type):
+        # Poll rather than read once: the startup process flushes recovery
+        # conflict stats to shared memory with a small delay.
+        # conflict_type names a column, so it has to be interpolated; the
+        # database name is a value and is bound.
+        standby.poll_query_until(
+            f"SELECT confl_{conflict_type} = 1 FROM pg_stat_database_conflicts "
+            "WHERE datname = $1",
+            TEST_DB,
+        )
+
+    def conflicting_session(*statements, expect, what):
+        """Open a standby session and put it in the state that will conflict.
+
+        ``statements`` run in a transaction; the last one's result must equal
+        ``expect``, which is how we know the cursor, lock or temp file the
+        conflict needs is really held before the primary does its thing.
+        """
+        session = standby.connect()
+        assert session.sql_batch("BEGIN", *statements)[-1] == expect, (
+            f"{what} established"
+        )
+        return session
+
+    expected_conflicts = 0
+
+    ## RECOVERY CONFLICT 1: Buffer pin conflict
+    expected_conflicts += 1
+
+    # Aborted INSERT on primary that will be cleaned up by vacuum. Has to be old
+    # enough so that there's not a snapshot conflict before the buffer pin
+    # conflict.
+    #
+    # The statements run on one held connection so the explicit transactions
+    # behave as in psql (a single multi-statement PQexec would merge them into
+    # one implicit transaction).
+    with primary.connect() as c:
+        c.sql("BEGIN")
+        c.sql(f"INSERT INTO {TABLE1} VALUES (1,0)")
+        c.sql("ROLLBACK")
+
+        # ensure flush, rollback doesn't do so
+        c.sql("BEGIN")
+        c.sql(f"LOCK {TABLE1}")
+        c.sql("COMMIT")
+    primary.wait_for_catchup(standby)
+
+    # DECLARE and use a cursor on standby, causing buffer with the only block of
+    # the relation to be pinned on the standby. FETCH FORWARD should return a 0
+    # since all values of b in the table are 0.
+    bg = conflicting_session(
+        f"DECLARE {CURSOR1} CURSOR FOR SELECT b FROM {TABLE1}",
+        f"FETCH FORWARD FROM {CURSOR1}",
+        expect=0,
+        what="buffer pin conflict: cursor with conflicting pin",
+    )
+
+    # to check the log starting now for recovery conflict messages
+    offset = standby.current_log_position()
+
+    # VACUUM FREEZE on the primary
+    primary.sql_oneshot(f"VACUUM FREEZE {TABLE1}")
+
+    # Wait for catchup. Existing connection will be terminated before replay is
+    # finished, so waiting for catchup ensures that there is no race between
+    # encountering the recovery conflict which causes the disconnect and checking
+    # the logfile for the terminated connection.
+    primary.wait_for_catchup(standby)
+    standby.wait_for_log("User was holding shared buffer pin for too long", offset)
+    bg.close()
+    check_conflict_stat("bufferpin")
+
+    ## RECOVERY CONFLICT 2: Snapshot conflict
+    expected_conflicts += 1
+    primary.sql_oneshot(
+        f"INSERT INTO {TABLE1} SELECT i, 0 FROM generate_series(1,20) i"
+    )
+
+    primary.wait_for_catchup(standby)
+
+    # DECLARE and FETCH from cursor on the standby
+    bg = conflicting_session(
+        f"DECLARE {CURSOR1} CURSOR FOR SELECT b FROM {TABLE1}",
+        f"FETCH FORWARD FROM {CURSOR1}",
+        expect=0,
+        what="snapshot conflict: cursor with conflicting snapshot",
+    )
+
+    # Do some HOT updates
+    primary.sql_oneshot(f"UPDATE {TABLE1} SET a = a + 1 WHERE a > 2")
+    offset = standby.current_log_position()
+
+    # VACUUM FREEZE, pruning those dead tuples
+    primary.sql_oneshot(f"VACUUM FREEZE {TABLE1}")
+
+    # Wait for attempted replay of PRUNE records
+    primary.wait_for_catchup(standby)
+    standby.wait_for_log(
+        "User query might have needed to see row versions that must be removed", offset
+    )
+    bg.close()
+    check_conflict_stat("snapshot")
+
+    ## RECOVERY CONFLICT 3: Lock conflict
+    expected_conflicts += 1
+
+    # acquire lock to conflict with
+    bg = conflicting_session(
+        f"LOCK TABLE {TABLE1} IN ACCESS SHARE MODE",
+        "SELECT 1",
+        expect=1,
+        what="lock conflict: conflicting lock",
+    )
+
+    offset = standby.current_log_position()
+
+    # DROP TABLE containing block which standby has in a pinned buffer
+    primary.sql_oneshot(f"DROP TABLE {TABLE1}")
+
+    primary.wait_for_catchup(standby)
+    standby.wait_for_log("User was holding a relation lock for too long", offset)
+    bg.close()
+    check_conflict_stat("lock")
+
+    ## RECOVERY CONFLICT 4: Tablespace conflict
+    expected_conflicts += 1
+
+    # DECLARE a cursor for a query which, with sufficiently low work_mem, will
+    # spill tuples into temp files in the temporary tablespace created during
+    # setup.
+    bg = conflicting_session(
+        "SET work_mem = '64kB'",
+        f"DECLARE {CURSOR1} CURSOR FOR SELECT count(*) FROM generate_series(1,6000)",
+        f"FETCH FORWARD FROM {CURSOR1}",
+        expect=6000,
+        what="tablespace conflict: cursor with conflicting temp file",
+    )
+
+    offset = standby.current_log_position()
+
+    # Drop the tablespace currently containing spill files for the query on the
+    # standby
+    primary.sql_oneshot(f"DROP TABLESPACE {TABLESPACE1}")
+
+    primary.wait_for_catchup(standby)
+    standby.wait_for_log(
+        "User was or might have been using tablespace that must be dropped", offset
+    )
+    bg.close()
+    check_conflict_stat("tablespace")
+
+    ## RECOVERY CONFLICT 5: Deadlock
+    expected_conflicts += 1
+
+    # Want to test recovery deadlock conflicts, not buffer pin conflicts. Without
+    # changing max_standby_streaming_delay it'd be timing dependent what we hit
+    # first
+    standby.append_conf(max_standby_streaming_delay=f"{pg_test_timeout_default()}s")
+    standby.pg_ctl("restart")
+
+    # Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
+    # lock on another relation in a prepared xact, so it's held continuously by
+    # the startup process. The standby psql will block acquiring that lock while
+    # holding a pin that vacuum needs, triggering the deadlock.
+    with primary.connect() as setup:
+        setup.sql(f"CREATE TABLE {TABLE1}(a int, b int)")
+        setup.sql(f"INSERT INTO {TABLE1} VALUES (1)")
+        with primary.connect() as c:
+            c.sql("BEGIN")
+            c.sql(f"INSERT INTO {TABLE1}(a) SELECT generate_series(1, 100) i")
+            c.sql("ROLLBACK")
+
+            # The prepared transaction holds the lock on TABLE2 continuously,
+            # independently of this session.
+            c.sql_batch("BEGIN", f"LOCK TABLE {TABLE2}", "PREPARE TRANSACTION 'lock'")
+        setup.sql(f"INSERT INTO {TABLE1}(a) VALUES (170)")
+        setup.sql("SELECT txid_current()")
+    primary.wait_for_catchup(standby)
+
+    bg = conflicting_session(
+        f"DECLARE {CURSOR1} CURSOR FOR SELECT a FROM {TABLE1}",
+        f"FETCH FORWARD FROM {CURSOR1}",
+        expect=1,
+        what="deadlock: cursor holding the pin vacuum needs",
+    )
+
+    # wait for lock held by the prepared transaction (blocks)
+    waiter = bg.background_sql(f"SELECT * FROM {TABLE2}")
+
+    try:
+        # just to make sure we're waiting for lock already
+        standby.poll_query_until(
+            "SELECT 'waiting' FROM pg_locks WHERE locktype = 'relation' AND NOT granted",
+            expected="waiting",
+        )
+
+        # VACUUM FREEZE will prune away rows, causing a buffer pin conflict, while
+        # standby psql is waiting on lock
+        offset = standby.current_log_position()
+        primary.sql_oneshot(f"VACUUM FREEZE {TABLE1}")
+
+        primary.wait_for_catchup(standby)
+        standby.wait_for_log(
+            "User transaction caused buffer deadlock with recovery.", offset
+        )
+    finally:
+        # Unlike the other conflicts this one resolves by canceling the
+        # statement (ERROR), not terminating the connection, so the session
+        # survives with an aborted transaction.
+        with pytest.raises(
+            LibpqError, match="canceling statement due to conflict with recovery"
+        ):
+            waiter.result()
+    # Disconnect so the backend exits and flushes its pending conflict stat to
+    # shared memory (an idle surviving backend would not flush it in time).
+    bg.close()
+    check_conflict_stat("deadlock")
+
+    # Clean up for the next tests.
+    primary.sql_oneshot("ROLLBACK PREPARED 'lock'")
+    standby.append_conf(max_standby_streaming_delay="50ms")
+    standby.pg_ctl("restart")
+
+    # Check the conflict count in pg_stat_database before the database is dropped.
+    assert (
+        standby.sql_oneshot(
+            "SELECT conflicts FROM pg_stat_database WHERE datname = $1", TEST_DB
+        )
+        == expected_conflicts
+    ), f"{expected_conflicts} recovery conflicts shown in pg_stat_database"
+
+    # RECOVERY CONFLICT 6: Database conflict. A live standby connection to the
+    # database is terminated when the drop is replayed.
+    db_conn = standby.connect()
+    db_conn.sql("SELECT 1")
+    offset = standby.current_log_position()
+
+    # The primary is done with the database, and has to be: DROP DATABASE
+    # refuses while anything is still connected, including the primary's own
+    # cached sql() connection. Switching the default back closes it. The
+    # standby's connection stays -- being terminated by the drop is the point.
+    primary.default_connection_options = {}
+    primary.sql(f"DROP DATABASE {TEST_DB}")
+
+    primary.wait_for_catchup(standby)
+    standby.wait_for_log(
+        "User was connected to a database that must be dropped", offset
+    )
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
deleted file mode 100644
index 7a740f69806..00000000000
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ /dev/null
@@ -1,333 +0,0 @@
-# Copyright (c) 2021-2026, PostgreSQL Global Development Group
-
-# Test that connections to a hot standby are correctly canceled when a
-# recovery conflict is detected Also, test that statistics in
-# pg_stat_database_conflicts are populated correctly
-
-use strict;
-use warnings FATAL => 'all';
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-
-
-# Set up nodes
-my $node_primary = PostgreSQL::Test::Cluster->new('primary');
-$node_primary->init(allows_streaming => 1);
-
-my $tablespace1 = "test_recovery_conflict_tblspc";
-
-$node_primary->append_conf(
-	'postgresql.conf', qq[
-allow_in_place_tablespaces = on
-log_temp_files = 0
-
-# for deadlock test
-max_prepared_transactions = 10
-
-# wait some to test the wait paths as well, but not long for obvious reasons
-max_standby_streaming_delay = 50ms
-
-temp_tablespaces = $tablespace1
-# Some of the recovery conflict logging code only gets exercised after
-# deadlock_timeout. The test doesn't rely on that additional output, but it's
-# nice to get some minimal coverage of that code.
-log_recovery_conflict_waits = on
-deadlock_timeout = 10ms
-]);
-$node_primary->start;
-
-my $backup_name = 'my_backup';
-
-$node_primary->safe_psql('postgres',
-	qq[CREATE TABLESPACE $tablespace1 LOCATION '']);
-
-$node_primary->backup($backup_name);
-my $node_standby = PostgreSQL::Test::Cluster->new('standby');
-$node_standby->init_from_backup($node_primary, $backup_name,
-	has_streaming => 1);
-
-$node_standby->start;
-
-my $test_db = "test_db";
-
-# use a new database, to trigger database recovery conflict
-$node_primary->safe_psql('postgres', "CREATE DATABASE $test_db");
-
-# test schema / data
-my $table1 = "test_recovery_conflict_table1";
-my $table2 = "test_recovery_conflict_table2";
-$node_primary->safe_psql(
-	$test_db, qq[
-CREATE TABLE ${table1}(a int, b int);
-INSERT INTO $table1 SELECT i % 3, 0 FROM generate_series(1,20) i;
-CREATE TABLE ${table2}(a int, b int);
-]);
-$node_primary->wait_for_replay_catchup($node_standby);
-
-
-# a longrunning psql that we can use to trigger conflicts
-my $psql_standby =
-  $node_standby->background_psql($test_db, on_error_stop => 0);
-my $expected_conflicts = 0;
-
-
-## RECOVERY CONFLICT 1: Buffer pin conflict
-my $sect = "buffer pin conflict";
-$expected_conflicts++;
-
-# Aborted INSERT on primary that will be cleaned up by vacuum. Has to be old
-# enough so that there's not a snapshot conflict before the buffer pin
-# conflict.
-
-$node_primary->safe_psql(
-	$test_db,
-	qq[
-	BEGIN;
-	INSERT INTO $table1 VALUES (1,0);
-	ROLLBACK;
-	-- ensure flush, rollback doesn't do so
-	BEGIN; LOCK $table1; COMMIT;
-	]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-my $cursor1 = "test_recovery_conflict_cursor";
-
-# DECLARE and use a cursor on standby, causing buffer with the only block of
-# the relation to be pinned on the standby
-my $res = $psql_standby->query_safe(
-	qq[
-    BEGIN;
-    DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
-    FETCH FORWARD FROM $cursor1;
-]);
-# FETCH FORWARD should have returned a 0 since all values of b in the table
-# are 0
-like($res, qr/^0$/m, "$sect: cursor with conflicting pin established");
-
-# to check the log starting now for recovery conflict messages
-my $log_location = -s $node_standby->logfile;
-
-# VACUUM FREEZE on the primary
-$node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
-
-# Wait for catchup. Existing connection will be terminated before replay is
-# finished, so waiting for catchup ensures that there is no race between
-# encountering the recovery conflict which causes the disconnect and checking
-# the logfile for the terminated connection.
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log("User was holding shared buffer pin for too long");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("bufferpin");
-
-
-## RECOVERY CONFLICT 2: Snapshot conflict
-$sect = "snapshot conflict";
-$expected_conflicts++;
-
-$node_primary->safe_psql($test_db,
-	qq[INSERT INTO $table1 SELECT i, 0 FROM generate_series(1,20) i]);
-$node_primary->wait_for_replay_catchup($node_standby);
-
-# DECLARE and FETCH from cursor on the standby
-$res = $psql_standby->query_safe(
-	qq[
-        BEGIN;
-        DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
-        FETCH FORWARD FROM $cursor1;
-        ]);
-like($res, qr/^0$/m, "$sect: cursor with conflicting snapshot established");
-
-# Do some HOT updates
-$node_primary->safe_psql($test_db,
-	qq[UPDATE $table1 SET a = a + 1 WHERE a > 2;]);
-
-# VACUUM FREEZE, pruning those dead tuples
-$node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
-
-# Wait for attempted replay of PRUNE records
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log(
-	"User query might have needed to see row versions that must be removed");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("snapshot");
-
-
-## RECOVERY CONFLICT 3: Lock conflict
-$sect = "lock conflict";
-$expected_conflicts++;
-
-# acquire lock to conflict with
-$res = $psql_standby->query_safe(
-	qq[
-        BEGIN;
-        LOCK TABLE $table1 IN ACCESS SHARE MODE;
-        SELECT 1;
-        ]);
-like($res, qr/^1$/m, "$sect: conflicting lock acquired");
-
-# DROP TABLE containing block which standby has in a pinned buffer
-$node_primary->safe_psql($test_db, qq[DROP TABLE $table1;]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log("User was holding a relation lock for too long");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("lock");
-
-
-## RECOVERY CONFLICT 4: Tablespace conflict
-$sect = "tablespace conflict";
-$expected_conflicts++;
-
-# DECLARE a cursor for a query which, with sufficiently low work_mem, will
-# spill tuples into temp files in the temporary tablespace created during
-# setup.
-$res = $psql_standby->query_safe(
-	qq[
-        BEGIN;
-        SET work_mem = '64kB';
-        DECLARE $cursor1 CURSOR FOR
-          SELECT count(*) FROM generate_series(1,6000);
-        FETCH FORWARD FROM $cursor1;
-        ]);
-like($res, qr/^6000$/m,
-	"$sect: cursor with conflicting temp file established");
-
-# Drop the tablespace currently containing spill files for the query on the
-# standby
-$node_primary->safe_psql($test_db, qq[DROP TABLESPACE $tablespace1;]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log(
-	"User was or might have been using tablespace that must be dropped");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("tablespace");
-
-
-## RECOVERY CONFLICT 5: Deadlock
-$sect = "startup deadlock";
-$expected_conflicts++;
-
-# Want to test recovery deadlock conflicts, not buffer pin conflicts. Without
-# changing max_standby_streaming_delay it'd be timing dependent what we hit
-# first
-$node_standby->adjust_conf(
-	'postgresql.conf',
-	'max_standby_streaming_delay',
-	"${PostgreSQL::Test::Utils::timeout_default}s");
-$node_standby->restart();
-$psql_standby->reconnect_and_clear();
-
-# Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
-# lock on another relation in a prepared xact, so it's held continuously by
-# the startup process. The standby psql will block acquiring that lock while
-# holding a pin that vacuum needs, triggering the deadlock.
-$node_primary->safe_psql(
-	$test_db,
-	qq[
-CREATE TABLE $table1(a int, b int);
-INSERT INTO $table1 VALUES (1);
-BEGIN;
-INSERT INTO $table1(a) SELECT generate_series(1, 100) i;
-ROLLBACK;
-BEGIN;
-LOCK TABLE $table2;
-PREPARE TRANSACTION 'lock';
-INSERT INTO $table1(a) VALUES (170);
-SELECT txid_current();
-]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-$res = $psql_standby->query_until(
-	qr/^1$/m, qq[
-    BEGIN;
-    -- hold pin
-    DECLARE $cursor1 CURSOR FOR SELECT a FROM $table1;
-    FETCH FORWARD FROM $cursor1;
-    -- wait for lock held by prepared transaction
-	SELECT * FROM $table2;
-    ]);
-ok(1,
-	"$sect: cursor holding conflicting pin, also waiting for lock, established"
-);
-
-# just to make sure we're waiting for lock already
-ok( $node_standby->poll_query_until(
-		'postgres', qq[
-SELECT 'waiting' FROM pg_locks WHERE locktype = 'relation' AND NOT granted;
-], 'waiting'),
-	"$sect: lock acquisition is waiting");
-
-# VACUUM FREEZE will prune away rows, causing a buffer pin conflict, while
-# standby psql is waiting on lock
-$node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log("User transaction caused buffer deadlock with recovery.");
-$psql_standby->reconnect_and_clear();
-check_conflict_stat("deadlock");
-
-# clean up for next tests
-$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
-$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
-	'50ms');
-$node_standby->restart();
-$psql_standby->reconnect_and_clear();
-
-
-# Check that expected number of conflicts show in pg_stat_database. Needs to
-# be tested before database is dropped, for obvious reasons.
-is( $node_standby->safe_psql(
-		$test_db,
-		qq[SELECT conflicts FROM pg_stat_database WHERE datname='$test_db';]),
-	$expected_conflicts,
-	qq[$expected_conflicts recovery conflicts shown in pg_stat_database]);
-
-
-## RECOVERY CONFLICT 6: Database conflict
-$sect = "database conflict";
-
-$node_primary->safe_psql('postgres', qq[DROP DATABASE $test_db;]);
-
-$node_primary->wait_for_replay_catchup($node_standby);
-
-check_conflict_log("User was connected to a database that must be dropped");
-
-
-# explicitly shut down psql instances gracefully - to avoid hangs or worse on
-# windows
-$psql_standby->quit;
-
-$node_standby->stop();
-$node_primary->stop();
-
-
-done_testing();
-
-sub check_conflict_log
-{
-	my $message = shift;
-	my $old_log_location = $log_location;
-
-	$log_location = $node_standby->wait_for_log(qr/$message/, $log_location);
-
-	cmp_ok($log_location, '>', $old_log_location,
-		"$sect: logfile contains terminated connection due to recovery conflict"
-	);
-}
-
-sub check_conflict_stat
-{
-	my $conflict_type = shift;
-	my $count = $node_standby->safe_psql($test_db,
-		qq[SELECT confl_$conflict_type FROM pg_stat_database_conflicts WHERE datname='$test_db';]
-	);
-
-	is($count, 1, "$sect: stats show conflict on standby");
-}
-- 
2.54.0

