Re: Per-table resync for logical replication subscriptions

From: Cagri Biroglu <cagri(dot)biroglu(at)adyen(dot)com>
To: Shlok Kyal <shlok(dot)kyal(dot)oss(at)gmail(dot)com>
Cc: "Hayato Kuroda (Fujitsu)" <kuroda(dot)hayato(at)fujitsu(dot)com>, Ajin Cherian <itsajin(at)gmail(dot)com>, "smithpb2250(at)gmail(dot)com" <smithpb2250(at)gmail(dot)com>, "pgsql-hackers(at)lists(dot)postgresql(dot)org" <pgsql-hackers(at)lists(dot)postgresql(dot)org>, Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>
Subject: Re: Per-table resync for logical replication subscriptions
Date: 2026-11-06 10:07:47
Message-ID: CAA36msotccBGaiej3n3qsUgxAqjtwpB-F3vYt8UoW0krHUFK9w@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Dear Shlok,

Thanks for the three reports. All are addressed in v14, attached, rebased
on current master.

A. Publisher down. The progress is in fact lost, even though nothing goes
visibly wrong straight away: v13 dropped the tablesync origin, whose
in-memory progress a rollback does not bring back, and only then failed to
connect. With the relation parked at FINISHEDCOPY, the origin's remote_lsn
went from 0/049107F0 to nothing, so catch-up would later resume from the
slot's confirmed position instead. The command now connects first, then
drops the slots, and drops the origins last, the order the tablesync worker
uses for its own. So a slot drop that fails after connecting also leaves
the origins alone, and an origin is dropped only once its relation has no
slot left to resume from. 039 checks that the progress survives both a
failed connection and a slot drop cancelled on the publisher;
poc_refresh_publisher_down_origin.sh shows the first case standalone.

B. Triggers. I kept TRUNCATE semantics: the command is run in a user's
session, and fires the triggers a TRUNCATE there would. The re-copy is made
by a tablesync worker running as replica, so a trigger that has to see both
halves can be ENABLE ALWAYS. The documentation now says so, and
poc_refresh_trigger_roles.sh shows which trigger fires for which half.

C. The crash. Thanks for the test. I found two more ways in: a FATAL while
a truncate trigger runs (pg_terminate_backend() is enough), and a trigger
whose EXCEPTION block rolls back a subtransaction. All three come from v13
setting the origin for the whole truncate, triggers included, and restoring
it afterwards. The commit and abort paths take a set origin for one
configured in the session and advance it, which fails when there is none:
a trigger has just reset it (your case), or there never was one (the other
two). The origin is now passed to ExecuteTruncateGuts() and set only while
the truncate's WAL record is inserted, so no trigger, error or abort sees
it. 039 covers your case, the other two, and a session with an origin of
its own. As a side effect, what the triggers write is no longer attributed
to the subscription's origin.

Also in v14: Song's fix to the lock-order test, and 039/040 run through
pgperltidy, which accounts for some of the noise in the test diff.

Best regards,
Cagri Biroglu

On Tue, Nov 3, 2026 at 3:37 PM Shlok Kyal <shlok(dot)kyal(dot)oss(at)gmail(dot)com> wrote:

> On Tue, 3 Nov 2026 at 15:24, Shlok Kyal <shlok(dot)kyal(dot)oss(at)gmail(dot)com> wrote:
> >
> > On Mon, 2 Nov 2026 at 01:35, Cagri Biroglu <cagri(dot)biroglu(at)adyen(dot)com>
> wrote:
> > >
> > > Hi Shlok ,
> > >
> > > thank you , fixed in v13, attached.
> > >
> > Hi Cagri,
> >
> > I further reviewed the v13 patch and found that ALTER SUBSCRIPTION ...
> > REFRESH TABLE truncates the target table by calling
> > ExecuteTruncateGuts(). Consequently, it executes BEFORE TRUNCATE and
> > AFTER TRUNCATE statement-level triggers on that table.
> > For example we have trigger:
> > CREATE TRIGGER t1_truncate_trigger
> > AFTER TRUNCATE ON t1
> > FOR EACH STATEMENT EXECUTE FUNCTION log_t1_trigger();
> >
> > And if t1 is part of subscription sub1, 'ALTER SUBSCRIPTION sub1
> > REFRESH TABLE t1;' can call the above trigger.
> >
> > Is this ok to allow triggers to be called when ALTER SUBSCRIPTION ...
> > REFRESH TABLE executed?
> >
> > I checked if any other ALTER SUBSCRIPTION command can execute any
> > triggers but I did not find any.
> > I noticed that apply workers can execute row-level INSERT, UPDATE, and
> > DELETE triggers, and statement-level TRUNCATE triggers, but only when
> > the table is configured as ENABLE REPLICA or ENABLE ALWAYS.
> > Should we have a similar way for the REFRESH TABLE case as well?
> >
> I did further testing and was able to hit an Assert.
> A BEFORE TRUNCATE trigger can change the session replication origin
> while REFRESH TABLE temporarily changes replorigin_xact_state.origin.
> AlterSubscription_refresh_table() saves replorigin_xact_state.origin,
> assigns the subscription origin while truncating and restores the
> saved value afterward. But, the trigger can reset the corresponding
> session state. Consequently, the saved origin ID is restored while
> session_replication_state remains NULL.
>
> During commit, replorigin_session_advance() can
> hit:Assert(session_replication_state != NULL);
> I have attached a perl test to reproduce the issue.
>
> Thanks,
> Shlok Kyal
>

Attachment Content-Type Size
v14-0002-refresh-table-partitions.patch application/octet-stream 46.6 KB
v14-0001-refresh-table.patch application/octet-stream 88.2 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Jakub Wartak 2026-11-06 10:10:27 Re: enhancing pg_basebackup speeds up to ~23Gbps (small fixes + io_uring/Direct I/O)
Previous Message Daniel Gustafsson 2026-11-06 10:04:52 Re: [PATCH] Fix use-after-free after failed pg_checksum_init