Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wxTmn-002kZv-2w for pgsql-hackers@arkaria.postgresql.org; Fri, 21 Aug 2026 18:16:14 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wxTml-00EUaE-2b for pgsql-hackers@arkaria.postgresql.org; Fri, 21 Aug 2026 18:16:11 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wxTml-00EUa6-0P for pgsql-hackers@lists.postgresql.org; Fri, 21 Aug 2026 18:16:11 +0000 Received: from fhigh-b5-smtp.messagingengine.com ([202.12.124.156]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wxTmi-00000001mSt-3Zk1 for pgsql-hackers@lists.postgresql.org; Fri, 21 Aug 2026 18:16:10 +0000 Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfhigh.stl.internal (Postfix) with ESMTP id CF02D7A012B; Fri, 21 Aug 2026 14:16:06 -0400 (EDT) Received: from phl-frontend-04 ([10.202.2.163]) by phl-compute-02.internal (MEProxy); Fri, 21 Aug 2026 14:16:06 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kurilemu.de; h= cc:cc:content-transfer-encoding:content-type:content-type:date :date:from:from:in-reply-to:in-reply-to:message-id:mime-version :reply-to:subject:subject:to:to; s=fm2; t=1787336166; x= 1787422566; bh=scfwkDcJJO1GayShZwwueljG2xwDiv9TwWdbDAIBBuE=; b=q BoMWY681E8a192RmyN0Ov3M4nT1eWZEUBYqnE42qsR3VjWeVl2Yd80mcIeHEhiwW t375LE4Sqn6oDqTRk4E/LdDIbYrPtd16zILixUKknSuIntnyUkgFVN/G7D2+GraV tZfnvz8ETVvnEhTea7+vnpl+LILSigtyG9256eO8z4n5lySwzbFxQUY7b65EpoXN ZSAqvP8U7zGCX7Kcy1n3XDI7dU2Bg5xdsQyvBYEqvnQ+V+yh66wWmH10vIdocZDk QqMwrIhZwT0llleN/abviNDhNs3yC9yHfA5FUj05NwOSYTNfjEHekRfSuU4gi8GY MXmhKkrs4yuRXAWNjyMAw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:content-type:date:date:feedback-id:feedback-id :from:from:in-reply-to:in-reply-to:message-id:mime-version :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm3; t=1787336166; x=1787422566; bh=s cfwkDcJJO1GayShZwwueljG2xwDiv9TwWdbDAIBBuE=; b=YSkTZUMVXTZ+ezqmu qeiCUCoP7pZqImx+nac5VYnjekPIv3Yx4Zu3uP0S8ayVl4j265UB6d5QwjnHNsVP 2adGYlRx5b5CM1XDLoIX5QpF14UyxPu2aMFs9dCzyNnD3xDUcpLid7W91+2fdI4c ozcfthmHI7bBJt7JV3+fEUHD7Y2fcuuTubuuwBJTdL+2N3/dNUzi2Syd/LuFLZtP 8JQbw/uusLyYUIqVlSbTzVJj4Pmc719KY+YT/HZS0gnn5lTjV79gla7cY6fd0gB/ luOr+EOElCLwO2MJBYzucsEiELniQzWN5JkV4i2QrveOSAdAq7oKc8jFw/8GkH3m eS1Ug== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: dmFkZTE8qfjEr0/PyU4kF8uYfj/lFdzjmN6vVWKRqv7fJSonL4QhkE5DDV6ml3EXNnIPIh FWPivGYPhb0YTw+mhPVsaT48tRb16YPNByJOJYA/fMUuNc0yixpt3bazWXclixL/EfdnOs w02gv0YbM4yikjChdY17SHM5VCkoZsvrNLZh4lTAXtRVcVUbzjes7w84iGPbEGqzB4Nxm4 LT1M3gi6hRSa/Cm1MSp3BpepR30pFpeh/vXbmQs0dsOCVi1o6QQttnIF0R5L3aT6Qywryp u7+G5T68JcYFQ3p3dLA9dmbj6oiJ3gC6pnAd9hGP4F14aCpkao57+HiHb1GtMsewyRQzIg c+kFS1tb+CsjweIRqnBWctEbzduZW/LnNjOt9BPAXYg5WyvfH2drCl4VO6ovvqHst/d8AL h0T5tSHfdTOGZG5SeT87DKTYRGI+ZhhTpRcm+QpMlPK31moI2AfF07ngAKT2N/TDkL5mte OGhvExq/yvZeqLx/Pi1w4lgE7pWPgYNXcN8HOmb7YD16UWo2XtaSV1IG8D+e1AxcdUkcRU LPUmTSxwjue23TE3rScexZ7QYBJ1YuS+BCn1cvcHt2G0VapXtwIBSnnq4ynpXPDTOUNGMD BQNUeSYMqmbE0HrbHKduQ3yf0OzYXcFQWqnA16LrYrRyQXX2r1JAp66zhreQ X-ME-Proxy: Feedback-ID: ie3de48e3:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Fri, 21 Aug 2026 14:16:06 -0400 (EDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kurilemu.de; s=schmee; t=1787336162; bh=Ydtan0SkfriUCee/16ezFXSw8tkqikCaLHrKkkJImxE=; h=Date:From:To:Cc:Subject:In-Reply-To:From; b=tRdmj0PZOlF5lKkkAJ/nR2up1nK70dnyfXAZgW+qRA8D57XT6SUw2A0n5QzfSJok2 zXs7nvE3bTyrVWGx4CtsdDj9GICEEyWVpc58alepPvsTt9ZytBw29/bv2hh4cQVv+p 5+Pj+HgwE8k7V1qs3ZHu+Rys6jVgzqBomUGX8ZMbAANn0joMUA54a27B2QdE/bAjBE cL6a0uIvwoftM6b0VMytnAZE6ZP3qXS4X4le3OswhJTCvFQO2X8VWG+4vb7gIxFQE0 rllN36Eu3517oGjfD4qrTumALV96XRybx4ZoGJU2WhpYprPd0Gg0XL8EhHp7rrDZt9 X0tZWzHS6QPhg== Received: by ida.kurilemu.internal (Postfix, from userid 1000) id B6A74B001BA; Fri, 21 Aug 2026 20:16:02 +0200 (CEST) Date: Fri, 21 Aug 2026 20:16:02 +0200 From: =?utf-8?Q?=C3=81lvaro?= Herrera To: Andres Freund Cc: Antonin Houska , pgsql-hackers@lists.postgresql.org, Mihail Nikalayeu Subject: Re: Race conditions in logical decoding Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="ic4mjwhsvm7ysii6" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <202603201543.t6gxppyyk66p@alvherre.pgsql> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --ic4mjwhsvm7ysii6 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit On 2026-Mar-20, Álvaro Herrera wrote: > Failing other ideas, I think we should just go with 0001. We'd need more > commentary on why is TransactionIdDidCommit() OK, when we haven't > scanned PGPROC for that xid, though. I spent some more time stepping through the motions here. In the test I saw, the problem is caused by the check for latestCompletedXid. The transaction we saw as committed in WAL has not yet been removed from ProcArray, which is what updates latestCompletedXid. So that makes TransactionIdIsInProgress() report that yes, the transaction is in progress, therefore we continue to wait in a loop forever, at least in synchronous replication. To recap: the problem was that returned a snapshot with a transaction recorded as committed, but which was not yet marked as such in CLOG, so when we did things like HeapTupleSatisfiesMVCC() with the snapshot so obtained, it would run TransactionIdDidCommit(), get false from it, and conclude that the transaction "must have aborted or crashed", therefore marking the tuple as HEAP_XMIN_INVALID. So what we do here is ensure that TransactionIdDidCommit() will return the correct value before giving the snapshot back. The other problem with this patch in the back of my mind was that we may be doing TransactionIdDidCommit() potentially for a lot of transactions. Instrumenting these code paths I saw that some tests in the suite would call the transam.c routine several thousand times, and some XIDs would repeat over and over. This may not sound like much, but we don't actually know what happens in production systems; and every transam.c call has the potential to do I/O to get the relevant CLOG page. And because we do this snapshot building in places like SnapBuildProcessChange(), it has the potential to do nasty. So I added a quick and dirty process-local cache: the list of transactions we tested on the previous cycle. We don't test nor wait for any transaction that's on that list, since evidently we must have tested it already and it cannot become uncommitted after that. All in all, we test for each potentially in-progress transaction just once per backend. So, what do you think of the attached? (On second thought, it may be a good idea to plant some of my explanation above in the new comment in SnapBuildBuildSnapshot. No time for that right now though.) -- Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/ "¿Cómo puedes confiar en algo que pagas y que no ves, y no confiar en algo que te dan y te lo muestran?" (Germán Poo) --ic4mjwhsvm7ysii6 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename=v3-0001-Fix-race-conditions-during-the-setup-of-logical-d.patch From 45252d5989dc0e06e0027142a1bfd4cfd744e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 21 Aug 2026 19:52:05 +0200 Subject: [PATCH v3] Fix race conditions during the setup of logical decoding. Although it's rather unlikely, it can happen that the snapshot builder considers transaction committed (according to WAL) before the commit could be recorded in CLOG. In an extreme case, snapshot can even be created and used in between. Since both snapshot and CLOG are needed for visibility checks, this inconsistency can make them work incorrectly. The typical symptom is that a transaction that the snapshot considers not running anymore is (per CLOG) considered aborted instead of committed. Thus a new tuple version can be evaluated as invisible (if xmin is incorrectly considered aborted) or a deleted tuple version can be evaluated as visible (if xmax is incorrectly considered aborted). This patch fixes the problem by checking if all the XIDs that the new snapshot considers committed are really committed per CLOG. If at least one is not, the check is repeated after a short delay. However, a single check is sufficient in almost all cases, so the performance impact should be minimal. Author: Antonin Houska Discussion: https://postgr.es/m/85833.1768840165@localhost --- src/backend/replication/logical/snapbuild.c | 74 ++++++++++++++++++- .../utils/activity/wait_event_names.txt | 1 + 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index f60bcf09605..31608c186dc 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -130,6 +130,7 @@ #include "access/xact.h" #include "common/file_utils.h" #include "miscadmin.h" +#include "nodes/pg_list.h" #include "pgstat.h" #include "replication/logical.h" #include "replication/reorderbuffer.h" @@ -365,6 +366,8 @@ SnapBuildBuildSnapshot(SnapBuild *builder) { Snapshot snapshot; Size ssize; + static List *xids_already_tested = NIL; + static List *new_xids_already_tested = NIL; Assert(builder->state >= SNAPBUILD_FULL_SNAPSHOT); @@ -378,7 +381,7 @@ SnapBuildBuildSnapshot(SnapBuild *builder) /* * We misuse the original meaning of SnapshotData's xip and subxip fields - * to make the more fitting for our needs. + * to make them more fitting for our needs. * * In the 'xip' array we store transactions that have to be treated as * committed. Since we will only ever look at tuples from transactions @@ -403,6 +406,75 @@ SnapBuildBuildSnapshot(SnapBuild *builder) snapshot->xmin = builder->xmin; snapshot->xmax = builder->xmax; + /* + * Although very unlikely, it's possible that a commit WAL record was + * decoded but CLOG is not aware of the commit yet. Should the CLOG update + * be delayed even more, visibility checks that use this snapshot could + * work incorrectly. Therefore we check the CLOG status here. + * + * We must not do this using TransactionIdIsInProgress()! The check there + * for latestCompletedXid would wreak havoc because the transaction we're + * interested in may not be out of ProcArray yet, but we must not wait for + * that. Doing the transam.c check directly is correct, though unusual. + * + * We don't want to repeatedly read the CLOG status for the same + * transaction, and it's easy to keep track of which ones we've already + * checked. Keep a list of the ones we test on each cycle, and use the + * list from the previous cycle to skip testing them now. + */ + for (int i = 0; i < builder->committed.xcnt; i++) + { + for (;;) + { + if (list_member_xid(xids_already_tested, builder->committed.xip[i])) + { + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(TopMemoryContext); + new_xids_already_tested = lappend_xid(new_xids_already_tested, + builder->committed.xip[i]); + MemoryContextSwitchTo(oldcxt); + break; + } + else if (TransactionIdDidCommit(builder->committed.xip[i])) + { + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(TopMemoryContext); + new_xids_already_tested = lappend_xid(new_xids_already_tested, + builder->committed.xip[i]); + MemoryContextSwitchTo(oldcxt); + break; + } + else + { + /* + * Note that the other process doesn't know we're waiting for + * them, so nothing is going to signal us out of this latch. + * Therefore use a short timeout. + */ + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | + WL_EXIT_ON_PM_DEATH, + 2L, + WAIT_EVENT_SNAPBUILD_CLOG); + ResetLatch(MyLatch); + } + CHECK_FOR_INTERRUPTS(); + } + } + + /* Swap these lists for next time */ + if (xids_already_tested != NIL) + list_free(xids_already_tested); + if (new_xids_already_tested != NIL) + { + xids_already_tested = new_xids_already_tested; + new_xids_already_tested = NIL; + } + else + xids_already_tested = NIL; + /* store all transactions to be treated as committed by this snapshot */ snapshot->xip = (TransactionId *) ((char *) snapshot + sizeof(SnapshotData)); diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 256b3a3c02e..55a9c8296b5 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -185,6 +185,7 @@ PG_SLEEP "Waiting due to a call to pg_sleep or a sibling fu RECOVERY_APPLY_DELAY "Waiting to apply WAL during recovery because of a delay setting." RECOVERY_RETRIEVE_RETRY_INTERVAL "Waiting during recovery when WAL data is not available from any source (pg_wal, archive or stream)." REGISTER_SYNC_REQUEST "Waiting while sending synchronization requests to the checkpointer, because the request queue is full." +SNAPBUILD_CLOG "Waiting for CLOG update before building snapshot." SPIN_DELAY "Waiting while acquiring a contended spinlock." VACUUM_DELAY "Waiting in a cost-based vacuum delay point." VACUUM_TRUNCATE "Waiting to acquire an exclusive lock to truncate off any empty pages at the end of a table vacuumed." -- 2.47.3 --ic4mjwhsvm7ysii6--