From 5c4e1c43b66a2a0e23c63e6382bb80d8be5a58d4 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Tue, 22 Sep 2026 21:16:06 +0000 Subject: [PATCH] Fix crash in logical replication apply worker at SERIALIZABLE. Commit fb60892f403 made check_exclusion_or_unique_constraint() read the conflicting tuple again with estate->es_snapshot at SERIALIZABLE, so that SSI sees the read. That assumed the EState came from ExecutorStart(), which always sets es_snapshot. The logical replication apply worker builds its EState with CreateExecutorState() and never set it, so detecting an insert_exists or update_exists conflict at SERIALIZABLE handed a NULL snapshot to heap_fetch() and the worker died with a segmentation fault. The subscriber then restarted, replayed the same change and crashed again, indefinitely. The apply worker starts its transactions at the default isolation level, so setting default_transaction_isolation to serializable for the subscriber's database or for the subscription owner is enough to reach this. To fix, set es_snapshot in the apply worker's EState to the snapshot that begin_replication_step() has already pushed for the change, as executor code expects. --- src/backend/replication/logical/worker.c | 8 +++++ src/test/subscription/t/035_conflicts.pl | 40 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 7781bb1c168..d166848ffe3 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -931,6 +931,14 @@ create_edata_for_relation(LogicalRepRelMapEntry *rel) estate->es_output_cid = GetCurrentCommandId(true); + /* + * Executor routines read the relation through es_snapshot. Give them the + * snapshot begin_replication_step() pushed for this change; the estate is + * gone again before that snapshot is popped, so there is no need to + * register it. + */ + estate->es_snapshot = GetActiveSnapshot(); + /* Prepare to catch AFTER triggers. */ AfterTriggerBeginQuery(); diff --git a/src/test/subscription/t/035_conflicts.pl b/src/test/subscription/t/035_conflicts.pl index 5804e38ed69..b9fc931c8cb 100644 --- a/src/test/subscription/t/035_conflicts.pl +++ b/src/test/subscription/t/035_conflicts.pl @@ -119,6 +119,46 @@ pass('multiple_unique_conflicts detected during update'); # Truncate table to get rid of the error $node_subscriber->safe_psql('postgres', "TRUNCATE conf_tab;"); +################################################## +# Test conflict detection at SERIALIZABLE +################################################## + +# The apply worker runs its transactions at the subscriber's default +# isolation level. Make that serializable, and restart the worker so that +# it picks up the new setting. +$node_subscriber->safe_psql('postgres', + "ALTER DATABASE postgres SET default_transaction_isolation = 'serializable'" +); +$node_subscriber->safe_psql('postgres', "ALTER SUBSCRIPTION sub_tab DISABLE"); +$node_subscriber->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" +); +$node_subscriber->safe_psql('postgres', "ALTER SUBSCRIPTION sub_tab ENABLE"); + +$log_offset = -s $node_subscriber->logfile; + +# Insert data in the subscriber table +$node_subscriber->safe_psql('postgres', + "INSERT INTO conf_tab VALUES (9,9,9);"); + +# Insert a row with the same key in the publisher table +$node_publisher->safe_psql('postgres', + "INSERT INTO conf_tab VALUES (9,10,11);"); + +# Confirm that this causes an error on the subscriber +$node_subscriber->wait_for_log( + qr/conflict detected on relation \"public.conf_tab\": conflict=insert_exists.* +.*Could not apply remote change: remote row \(9, 10, 11\).* +.*Key already exists in unique index \"conf_tab_pkey\", modified in transaction .*: key \(a\)=\(9\), local row \(9, 9, 9\)./, + $log_offset); + +pass('insert_exists detected at serializable isolation'); + +# Truncate table to get rid of the error, and restore the default isolation +# level +$node_subscriber->safe_psql('postgres', "TRUNCATE conf_tab;"); +$node_subscriber->safe_psql('postgres', + "ALTER DATABASE postgres RESET default_transaction_isolation"); ################################################## # Test multiple_unique_conflicts due to INSERT on a leaf partition -- 2.55.0