From 486640303719e843af97904252ab44f605629f9b Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Thu, 1 Oct 2026 08:17:48 +0000 Subject: [PATCH v2] 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, skip the read when the EState has no snapshot. Callers that set up their own EState did not need es_snapshot before fb60892f403, and the apply worker only replays changes the publisher has already committed, so there is nothing for SSI to record there anyway. Extensions that call ExecCheckIndexConstraints() with an EState of their own are in the same position, so this keeps working for them too. --- src/backend/executor/execIndexing.c | 8 ++++- src/test/subscription/t/035_conflicts.pl | 40 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c index d0a714a42f2..b3a92283f73 100644 --- a/src/backend/executor/execIndexing.c +++ b/src/backend/executor/execIndexing.c @@ -920,8 +920,14 @@ retry: * read. Read the tuple again with the query snapshot to * record it. The result is of no interest here, the caller * checks visibility itself. + * + * Callers that build their own EState, such as the logical + * replication apply worker, may not have set es_snapshot. + * Such callers are not executing a query, so there is + * nothing for SSI to record; skip the read in that case. */ - if (IsolationIsSerializable()) + if (IsolationIsSerializable() && + estate->es_snapshot != InvalidSnapshot) { INJECTION_POINT("check-exclusion-or-unique-constraint-conflict", NULL); (void) table_tuple_fetch_row_version(heap, conflictTid, 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