Subject: Re: Race conditions in logical decoding

From: Álvaro Herrera <[email-masked]>
Date: Thu, 22 Jan 2026 20:59:21 +0100

On 2026-Jan-20, Antonin Houska wrote:

> Antonin Houska <[email-masked]> wrote:
> 
> > I'm not sure yet how to fix the problem. I tried to call XactLockTableWait()
> > from SnapBuildAddCommittedTxn() (like it happens in SnapBuildWaitSnapshot()),
> > but it made at least one regression test (subscription/t/010_truncate.pl)
> > stuck - probably a deadlock. I can spend more time on it, but maybe someone
> > can come up with a good idea sooner than me.
> 
> Attached here is what I consider a possible fix - simply wait for the CLOG
> update before building a new snapshot.
> 
> Unfortunately I have no idea right now how to test it using the isolation
> tester. With the fix, the additional waiting makes the current test
> block. (And if a step is added that unblock the session, it will not reliably
> catch failure to wait.)
> 
> -- 
> Antonin Houska
> Web: https://www.cybertec-postgresql.com
> 

> @@ -400,6 +400,47 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
>  	snapshot->xmin = builder->xmin;
>  	snapshot->xmax = builder->xmax;
>  
> +	/*
> +	 * Although it's 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.
> +	 */
> +	while (true)
> +	{
> +		bool	found = false;
> +
> +		for (int i = 0; i < builder->committed.xcnt; i++)
> +		{
> +			/*
> +			 * XXX Is it worth remembering the XIDs that appear to be
> +			 * committed per CLOG and skipping them in the next iteration of
> +			 * the outer loop? Not sure it's worth the effort - a single
> +			 * iteration is enough in most cases.
> +			 */
> +			if (unlikely(!TransactionIdDidCommit(builder->committed.xip[i])))
> +			{
> +				found = true;
> +
> +				/*
> +				 * Wait a bit before going to the next iteration of the outer
> +				 * loop. The race conditions we address here is pretty rare,
> +				 * so we shouldn't need to wait too long.
> +				 */
> +				(void) WaitLatch(MyLatch,
> +								 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
> +								 10L,
> +								 WAIT_EVENT_SNAPBUILD_CLOG);
> +				ResetLatch(MyLatch);
> +
> +				break;
> +			}
> +		}
> +
> +		if (!found)
> +			break;
> +	}

I think this algorithm is strange -- if you do have to wait more than
once for one transaction, it would lead to doing the
TransactionIdDidCommit again times for _all_ transactions by starting
the inner loop from scratch, which sounds really wasteful.  Why not nest
the for() loops the other way around?  Something like this perhaps,

    for (int i = 0; i < builder->committed.xcnt; i++)
    {
        for (;;)
        {
            if (TransactionIdDidCommit(builder->committed.xip[i]))
                break;
            else
            {
                (void) WaitLatch(MyLatch,
                                 WL_LATCH_SET, WL_TIMEOUT, WL_EXIT_ON_PM_DEATH,
                                 10L,
                                 WAIT_EVENT_SNAPBUILD_CLOG);
                ResetLatch(MyLatch);
            }
            CHECK_FOR_INTERRUPTS();
        }
    }

This way you wait repeatedly for one transaction until it is marked
committed; and once it does, you don't test it again.

I also wondered if it would make sense to get rid of the memcpy, given
that we're doing so much work per xid anyway it won't make any visible
difference (I believe), and do the copy per XID there, like

            if (TransactionIdDidCommit(builder->committed.xip[i]))
	    {
	        snapshot->xip[i] = builder->committed.xip[i];
                break;
	    }
            else
            ...

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Sallah, I said NO camels! That's FIVE camels; can't you count?"
(Indiana Jones)

---

From: Antonin Houska <[email-masked]>
Date: Fri, 23 Jan 2026 07:33:32 +0100

Álvaro Herrera <[email-masked]> wrote:

> the for() loops the other way around?

I'm quite sure I wanted to iterate through committed.xnt in the outer loop,
but probably got distracted by something else and messed things up.

> Something like this perhaps,
> 

Sure, that's much beter. Thanks.