From 2da7253dd457963e70158ea51ae7fcaa3292ba77 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Wed, 29 Jul 2026 16:10:49 +0500 Subject: [PATCH v3 2/2] Optionally wait for synchronous replication after query cancel A transaction waiting for synchronous replication has already committed locally. Ending that wait on query cancellation releases its locks and makes its changes visible before the WAL necessarily reaches a standby. A retry can then appear to succeed even though a subsequent failover can lose the change. Add synchronous_replication_wait_on_query_cancel, defaulting to off to preserve the existing behavior. When it is on, report but ignore query cancellation during a synchronous replication wait. Continue to honor session termination and postmaster shutdown. Discussion: https://postgr.es/m/0B44E464-BA62-4056-9465-3320DD2D0302@yandex-team.ru --- doc/src/sgml/config.sgml | 25 ++++++++ src/backend/replication/syncrep.c | 30 ++++++--- src/backend/utils/misc/guc_parameters.dat | 6 ++ src/backend/utils/misc/postgresql.conf.sample | 3 + src/include/replication/syncrep.h | 1 + src/test/recovery/t/007_sync_rep.pl | 64 +++++++++++++++++++ 6 files changed, 120 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index ef950f88a04..3bc0ec3f040 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -5095,6 +5095,31 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows + + synchronous_replication_wait_on_query_cancel (boolean) + + synchronous_replication_wait_on_query_cancel configuration parameter + + + + + When this parameter is off (the default), a query cancellation request + ends a wait for synchronous replication. The transaction may already + have committed locally, so ending the wait can make its changes visible + before the WAL has reached the synchronous standbys. + + + When this parameter is on, query cancellation requests are reported but + ignored while waiting for synchronous replication. Administrator + commands that terminate the session, including server shutdown, still + end the wait. This parameter can only be set in the + postgresql.conf file or on the server command + line. + + + + synchronous_standby_names (string) diff --git a/src/backend/replication/syncrep.c b/src/backend/replication/syncrep.c index 2864bac3b8b..6e95461b939 100644 --- a/src/backend/replication/syncrep.c +++ b/src/backend/replication/syncrep.c @@ -89,6 +89,7 @@ /* User-settable parameters for sync rep */ char *SyncRepStandbyNames; +bool SyncRepWaitOnQueryCancel = false; int post_recovery_sync_level = SYNCHRONOUS_COMMIT_OFF; #define SyncStandbysDefined() \ @@ -150,6 +151,7 @@ void SyncRepWaitForLSN(XLogRecPtr lsn, bool commit) { int mode; + bool query_cancel_reported = false; /* * This should be called while holding interrupts during a transaction @@ -320,19 +322,29 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit) } /* - * It's unclear what to do if a query cancel interrupt arrives. We - * can't actually abort at this point, but ignoring the interrupt - * altogether is not helpful, so we just terminate the wait with a - * suitable warning. + * We can't actually abort at this point. By default, terminate the + * wait with a suitable warning. If configured otherwise, continue + * waiting so that query cancellation cannot make locally committed + * data visible before it has been replicated. */ if (QueryCancelPending) { QueryCancelPending = false; - ereport(WARNING, - (errmsg("canceling wait for synchronous replication due to user request"), - errdetail("The transaction has already committed locally, but might not have been replicated to the standby."))); - SyncRepCancelWait(); - break; + if (!SyncRepWaitOnQueryCancel) + { + ereport(WARNING, + (errmsg("canceling wait for synchronous replication due to user request"), + errdetail("The transaction has already committed locally, but might not have been replicated to the standby."))); + SyncRepCancelWait(); + break; + } + if (!query_cancel_reported) + { + ereport(WARNING, + (errmsg("ignoring request to cancel wait for synchronous replication"), + errdetail_internal("The locally written WAL record has not been replicated according to \"synchronous_standby_names\"."))); + query_cancel_reported = true; + } } /* diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index bae9e178746..55ac4b7c91c 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2975,6 +2975,12 @@ assign_hook => 'assign_synchronous_commit', }, +{ name => 'synchronous_replication_wait_on_query_cancel', type => 'bool', context => 'PGC_SIGHUP', group => 'REPLICATION_PRIMARY', + short_desc => 'Continues waiting for synchronous replication after query cancellation.', + variable => 'SyncRepWaitOnQueryCancel', + boot_val => 'false', +}, + { name => 'synchronous_standby_names', type => 'string', context => 'PGC_SIGHUP', group => 'REPLICATION_PRIMARY', short_desc => 'Number of synchronous standbys and list of names of potential synchronous ones.', flags => 'GUC_LIST_INPUT', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index a51c7b1d185..f432fdabc34 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -376,6 +376,9 @@ # method to choose sync standbys, number of sync standbys, # and comma-separated list of application_name # from standby(s); '*' = all +#synchronous_replication_wait_on_query_cancel = off + # continue synchronous replication waits + # after query cancellation #synchronized_standby_slots = '' # streaming replication standby server slot # names that logical walsender processes will wait for diff --git a/src/include/replication/syncrep.h b/src/include/replication/syncrep.h index f6cf18b636e..6111cd74659 100644 --- a/src/include/replication/syncrep.h +++ b/src/include/replication/syncrep.h @@ -72,6 +72,7 @@ typedef struct SyncRepConfigData } SyncRepConfigData; extern PGDLLIMPORT SyncRepConfigData *SyncRepConfig; +extern PGDLLIMPORT bool SyncRepWaitOnQueryCancel; extern PGDLLIMPORT int post_recovery_sync_level; /* user-settable parameters for synchronous replication */ diff --git a/src/test/recovery/t/007_sync_rep.pl b/src/test/recovery/t/007_sync_rep.pl index 224f1a82e06..b169f02a6ab 100644 --- a/src/test/recovery/t/007_sync_rep.pl +++ b/src/test/recovery/t/007_sync_rep.pl @@ -218,6 +218,70 @@ standby4|1|quorum), 'all standbys are considered as candidates for quorum sync standbys', 'ANY 2(*)'); +# Check that a query cancel does not release a transaction which has +# committed locally but is still waiting for synchronous replication. +$node_primary->safe_psql( + 'postgres', q[ + CREATE TABLE sync_rep_cancel_test (a int); + ALTER SYSTEM SET synchronous_replication_wait_on_query_cancel = on; + ALTER SYSTEM SET synchronous_standby_names = 'unavailable'; + SELECT pg_reload_conf();]); + +my $cancel_session = + $node_primary->background_psql('postgres', on_error_stop => 0); +$cancel_session->query_until( + qr/insert_started/, q[ + SET application_name = 'sync_rep_cancel_test'; + \echo insert_started + INSERT INTO sync_rep_cancel_test VALUES (1); +]); + +$node_primary->poll_query_until( + 'postgres', + q[SELECT count(*) = 1 + FROM pg_stat_activity + WHERE application_name = 'sync_rep_cancel_test' + AND wait_event = 'SyncRep']) + or die "backend did not start waiting for synchronous replication"; + +is( + $node_primary->safe_psql( + 'postgres', + q[SELECT pg_cancel_backend(pid) + FROM pg_stat_activity + WHERE application_name = 'sync_rep_cancel_test']), + 't', + 'cancel request was delivered to synchronous replication waiter'); + +$node_primary->poll_query_until( + 'postgres', + q[SELECT count(*) = 1 + FROM pg_stat_activity + WHERE application_name = 'sync_rep_cancel_test' + AND wait_event = 'SyncRep']) + or die "query cancel ended synchronous replication wait"; + +is( + $node_primary->safe_psql('postgres', + 'SELECT count(*) FROM sync_rep_cancel_test'), + '0', + 'locally committed transaction remains invisible after query cancel'); + +$node_primary->safe_psql( + 'postgres', q[ + ALTER SYSTEM RESET synchronous_standby_names; + SELECT pg_reload_conf();]); +$cancel_session->quit; +is( + $node_primary->safe_psql('postgres', + 'SELECT count(*) FROM sync_rep_cancel_test'), + '1', + 'transaction completes after synchronous replication is disabled'); +like( + $cancel_session->{stderr}, + qr/ignoring request to cancel wait for synchronous replication/, + 'ignored query cancel is reported'); + # Check that ordinary connections are rejected after crash recovery until # the end-of-recovery LSN reaches the configured synchronous standby. for my $standby ( -- 2.50.1 (Apple Git-155)