| From: | Peter Smith <smithpb2250(at)gmail(dot)com> |
|---|---|
| To: | Amit Kapila <amit(dot)kapila16(at)gmail(dot)com> |
| Cc: | vignesh C <vignesh21(at)gmail(dot)com>, Dilip Kumar <dilipbalaut(at)gmail(dot)com>, "Zhijie Hou (Fujitsu)" <houzj(dot)fnst(at)fujitsu(dot)com>, Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>, saurabh singh <saurabh(dot)singh214(at)gmail(dot)com>, Robert Haas <robertmhaas(at)gmail(dot)com>, Nisha Moond <nisha(dot)moond412(at)gmail(dot)com>, Bharath Rupireddy <bharath(dot)rupireddyforpostgres(at)gmail(dot)com>, PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>, shveta malik <shveta(dot)malik(at)gmail(dot)com> |
| Subject: | Re: Proposal: Conflict log history table for Logical Replication |
| Date: | 2026-10-13 07:01:01 |
| Message-ID: | CAHut+PtxaQXNxz0QVw+M8DUJ0BWLvi3=feRgcpVXOaYKo9c_ew@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
On Mon, Aug 24, 2026 at 7:32 PM Amit Kapila <amit(dot)kapila16(at)gmail(dot)com> wrote:
>
> On Fri, Aug 21, 2026 at 12:13 PM vignesh C <vignesh21(at)gmail(dot)com> wrote:
> >
> > Thanks for the patch Dilip.
> > Logical replication breaks when conflict_log_destination = 'table' and
> > an update_origin_differs conflict involves a large row.
> > When the conflict occurs on a row containing a large value (~190 MB),
> > the conflict log table path converts the old row data into a string
> > representation. During this conversion, the resulting string buffer
> > grows beyond 1 GB and exceeds PostgreSQL's maximum StringInfo buffer
> > size of 1073741823 bytes.
> >
> > This results in:
> > "ERROR: string buffer exceeds maximum allowed length (1073741823 bytes)"
> > "DETAIL: Cannot enlarge string buffer containing 1073741808 bytes by
> > 32 more bytes."
> >
> > As a result, the logical replication apply worker exits, and
> > replication cannot make further progress.
> > Interestingly, the same scenario works successfully with
> > conflict_log_destination = 'log'. In that case, the conflict is logged
> > successfully and logical replication continues without any error.
> >
> > Reproducer
> > The issue can be reproduced with the following steps:
> > 1) Set up logical replication with conflict_log_destination = 'table'
> > and create the following table:
> > "CREATE TABLE tab (a int PRIMARY KEY, b text);"
> > 2) On the subscriber, enable track_commit_timestamp = on to detect
> > update_origin_differs conflicts.
> > 3) Insert a row on the publisher:
> > "INSERT INTO tab VALUES (1, 'small');"
> > 4) On the subscriber, update the replicated row with a large value:
> > "UPDATE tab SET b = repeat(chr(1), 190_000_000) WHERE a = 1;"
> >
>
> Here, the chr(1)'s special value makes the string longer. So, the
> failure is not really about row size, it is about JSON escaping
> expansion. escape_json* turns every byte below 0x20 into a six-byte
> \uXXXX sequence. The reproducer's value is 190MB of chr(1), which
> expands to ~1.14GB against the 1073741823-byte StringInfo limit,
> matching the reported error exactly. As per my understanding, this
> cannot be fixed by enlarging a buffer. The result has to become a json
> Datum, and varlena is capped at the same 1GB, so a tuple whose JSON
> form exceeds 1GB cannot be stored in a json column at all. It is a
> representational ceiling. So, the possible options to fix this are:
> (a) Don't LOG any column_value_size greater than 2K. (b) Truncate
> bigger column values(>2K) to 2K or even shorter number 64 bytes. (c)
> LOG only index/RI columns but what should we do for large columns on
> the subscriber side or if RI is set to FULL?
>
> If we choose option (b), then a truncated value in a queryable table
> silently produces wrong results: WHERE tuple->>'col' = ..., joins and
> old-vs-new diffs all answer incorrectly with no way for the consumer
> to tell. In a server log a human can see the value is clipped; in SQL
> nothing can. I think option (c) is not workable because of reasons
> mentioned along with option (c). I would prefer to go with option (a)
> aka store a value verbatim when it is below a cap; above the cap,
> record no value plus an explicit marker. Every value present in the
> CLT is then byte-exact, and every value that could not be stored says
> so.
>
> If we would like to go with option (a) then the next thing to decide
> is how to display/show/represent omitted columns.
>
> (1) The first option is to mark the omission on the value itself, e.g.
> "b": {"omitted": true, "length": 190000000}. This is self-locating
> (the same column can be oversized in one payload and fine in another),
> it cannot collide with a user column name (any scheme that adds a
> metadata key alongside column-name keys is unsafe, since a real column
> can be named anything), it travels with the row, and it fails loudly
> rather than looking like a genuine NULL. (2) The other option is to
> use a separate column like omitted_columns. There are three
> independent JSON payloads per row — replica_identity, remote_tuple,
> and local_conflicts (an array whose elements each contain a tuple).
> That's what creates the problems because this option can't say which
> payload. (3) Yet another option is to simply LOG the omitted column
> names but may be difficult for users to find out. Or maybe some
> combination of these options.
>
> The one benefit of option (2) is cheap filtering. WHERE
> omitted_columns IS NOT NULL to find incomplete rows is trivial,
> whereas with in-value markers you'd need a JSON search. So, I suggest
> to go with a mix of option 1 and 2, put the detail on the value — "b":
> {"omitted": true, "length": 190000000} and add a plain bool (say
> has_omitted_values) purely as a filter.
>
Shveta pointed me to this post explaining about why the oversized
columns cannot simply be truncated.
I was wondering if a middle-ground solution would be useful. One where
the data is still flagged as truncated has the advantages of Amit's
proposal in that it cannot be confused with byte-exact data, but it
still carries some truncated value which might (?) be helpful to a
human reader.
e.g.
Option1 - This option is no good because JSON is too large
"b" : "some really long value that it too long and would exceed limits"
Option 2 - This option is no good because SQL cannot tell if it is
really truncated or not.
"b" : "some really long value that ..."
Option 3. This option is what Ami proposed in the above mail.
"b" : {"omitted": true, "length": 190000000}
Option 4 - This option is the middle-ground that I was wondering about.
"b" : {"truncated": true, "length": 190000000, "prefix": "some really
long value that ..." }
======
Kind Regards,
Peter Smith.
Fujitsu Australia
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Fujii Masao | 2026-10-13 07:17:02 | Re: 041_checkpoint_at_promote.pl might fail due to race condition on child kill |
| Previous Message | Hayato Kuroda (Fujitsu) | 2026-10-13 06:54:30 | RE: Test publication.sql racing with subscription.sql might fail due to cache lookup error |