Re: Proposal: Conflict log history table for Logical Replication

From: Dilip Kumar <dilipbalaut(at)gmail(dot)com>
To: Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>
Cc: shveta malik <shveta(dot)malik(at)gmail(dot)com>, vignesh C <vignesh21(at)gmail(dot)com>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, "Zhijie Hou (Fujitsu)" <houzj(dot)fnst(at)fujitsu(dot)com>, saurabh singh <saurabh(dot)singh214(at)gmail(dot)com>, Robert Haas <robertmhaas(at)gmail(dot)com>, Peter Smith <smithpb2250(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>
Subject: Re: Proposal: Conflict log history table for Logical Replication
Date: 2026-09-17 14:36:34
Message-ID: CAFiTN-uZfzOC9Fo6ga4RhR0RohEVWWmegHEP3n=F0uKiJTzWqA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Wed, Sep 16, 2026 at 8:56 AM Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com> wrote:

Thanks Sawada-san for you detailed analysis.

> On Wed, Sep 9, 2026 at 8:41 AM Dilip Kumar <dilipbalaut(at)gmail(dot)com> wrote:
> >
> > On Tue, Sep 1, 2026 at 4:03 PM shveta malik <shveta(dot)malik(at)gmail(dot)com> wrote:
> > >
> > Here is detailed analysis and summary of the problem and all the
> > alternatives we tried. The problem is that converting a
> > TupleTableSlot to JSON can result in rendered JSON text exceeding
> > PostgreSQL's MaxAllocSize limit (1 GB). When that happens, memory
> > allocation (palloc() or enlargeStringInfo()) throws a hard ERROR.
> >
> > This issue is not unique to conflict logging:
> > 1) pgoutput does not handle this: if a row's formatted textual
> > representation exceeds 1 GB in logicalrep_write_tuple(),
> > OidOutputFunctionCall() or palloc() fails with an unrecoverable hard
> > error.
> > 2) For conflict logging, converting to JSON exacerbates the problem
> > because a) every byte below 0x20 expands into a 6-byte \uXXXX
> > sequence. b) JSONB values (e.g., repeated numeric expansions like
> > 1e131071), arrays, and composites can be compact on disk but expand to
> > hundreds of megabytes or gigabytes when serialized to text. c)
> > user-Defined Types (UDTs): A compact binary UDT (e.g., a run-length
> > encoded vector) can expand into gigabytes inside its typoutput
> > function.
> > 3) Other conflict logging extensions (e.g., pgactive and pgEdge) also
> > have this issue, and none of them handle it.
> >
> > Because storing multi-gigabyte or hundreds-of-megabytes values in a
> > conflict log table is neither practical nor desirable for post-mortem
> > debugging, we have been exploring several options to gracefully handle
> > oversized attributes without erroring out. Below is a summary of the
> > options explored, along with their pros and cons.
>
> Thank you for summarizing the potential solutions. It made the problem
> much clearer. IIUC it can technically happen that the constructed
> JSONB data exceed 1GB limitation, but it's a relatively rare case in
> practice.
>
> > Option 1: Attribute-Level Size Capping via ErrorSaveContext (escontext)
> > Enforce a reasonable threshold (e.g., 16 KB) per attribute during JSON
> > conversion. Pass an ErrorSaveContext to an extended serialization
> > function (datum_to_json_extended()). As serialization recurses through
> > arrays, composites, or JSONB containers, cumulative size is monitored.
> > If an attribute exceeds the limit, errsave() records the soft error
> > and returns (Datum) 0. The caller detects
> > SOFT_ERROR_OCCURRED(&escontext) and cleanly replaces that attribute in
> > the outer JSON tuple with an omission object: {"omitted": true,
> > "length": ...}.
> > Pros:
> > - Retains the full tuple structure and all other normal-sized
> > columns for analysis.
> > - Cleanly handles unbounded recursion in nested structures (arrays,
> > composites, and JSONB) by aborting early.
> > Cons:
> > - Does not protect against typoutput failures: standard PostgreSQL
> > output functions invoked via OidOutputFunctionCall() do not accept an
> > ErrorSaveContext parameter.
> > - If a UDT (or built-in bytea with a large payload in hex format)
> > exceeds 1 GB inside its typoutput function, palloc() throws a hard
> > ERROR before size checks run; escontext cannot intercept it.
>
> This is the most complete answer if we do want full-tuple logging. But
> there is a large amount of new code in the JSON code required for v1,
> and as you note it still doesn't close the typoutput hole. I don't
> think the complexity is justified by the extra diagnostic value.

I agree with your analysis.

> >
> > Option 2: Record Only Replica Identity (RI) Columns Instead of Full Tuples
> > Instead of serializing entire remote and local tuples to JSON, only
> > serialize the Replica Identity key columns (typically Primary Key or
> > Unique Index attributes) identifying the conflicting row.
> > Pros:
> > - Drastically reduces conflict log storage overhead and serialization cost.
> > - For standard integer, UUID, or short text keys, the payload is
> > tiny and never approaches memory limits.
> > Cons:
> > - Does not fully eliminate the problem: Replica Identity can be
> > defined on a UDT or large composite key. If that key attribute expands
> > to > 1 GB in typoutput, conflict logging will still error out. In
> > short, this leaves us with the same problem as Option 1.
>
> I think recording only the RI columns would work for conflict
> detection/resolution purposes. Showing the RI columns of a RI FULL
> table would still have the problem, so we might want to either show
> nothing or ask users to disable conflict history logging in that case.
> But recording only the RI columns makes an already rare case
> even rarer, and would cover most use cases. We can improve such cases
> later in a separate patch.

+1, I think storing RI make sense and when there is RI FULL we may
store the NULL value and makr RI FULL as true. Yes and we can always
improve this based on feedback once its being used.

> >
> > Option 3: Whitelist Only Fixed-Length / Safe Built-in Data Types in v1
> > In the initial version of conflict logging, only serialize columns
> > with guaranteed small, bounded types (e.g., fixed-length types like
> > int2, int4, int8, float4, float8, bool, date, timestamp, uuid). Any
> > varlena type, container, or UDT is automatically omitted without
> > invoking its output function.
> > Pros:
> > - Completely immune to palloc() 1 GB overflow by construction.
> > - Simple to implement with zero chance of erroring out.
> > - Safe baseline that can be incrementally expanded in future releases.
> > Cons:
> > - Could be restrictive: common types like text, varchar, jsonb, and
> > numeric are omitted even when their values
> > are just a few bytes (e.g., a 10-character varchar column).
>
> Text and numeric keys are the common case, and omitting them would be
> worse to me than the problem we are solving.

Yeah make sense.

> > Option 4: Wrap Attribute Serialization in PG_TRY() / PG_CATCH()
> > While serializing each attribute of the tuple to JSON, wrap the
> > conversion (specifically OidOutputFunctionCall()) inside an internal
> > subtransaction with a PG_TRY() / PG_CATCH() block:
> > Pros:
> > - Catches all hard errors, including 1 GB palloc() exhaustion inside
> > uncooperative typoutput functions, memory allocation failures, or
> > corrupted data.
> > - Enables full support for all data types (built-in, JSONB, varlena,
> > and UDTs) without risking apply worker retry loops.
> > - Does not require changing PostgreSQL's global typoutput function
> > signature to support escontext.
> > Cons:
> > - Using PG_TRY() and internal subtransactions adds management
> > overhead (though conflict logging is an exceptional path, not the main
> > transaction fast-path).
> > - Consumes Transaction IDs (subXIDs), though read-only in-memory
> > subtransactions are relatively lightweight.
>
> I don't think we can classify the error reliably. enlargeStringInfo()
> reports ERRCODE_PROGRAM_LIMIT_EXCEEDED, an oversized palloc() request
> reports ERRCODE_INTERNAL_ERROR, and a genuine allocation failure
> reports ERRCODE_OUT_OF_MEMORY. We cannot tell "this value was too
> large to render" from "this backend is really out of memory" or from a
> bug in some type's output function, and silently swallowing the latter
> two in an apply worker seems worse than the disease.
>
> Overall, I prefer option 2 and deal with the very rare cases in a
> separate patch if necessary.

Yes, that makes sense. I have prepared a top-up patch using approach
2, IMHO the edge case should realistically only trigger when a
user-defined type’s output function produces an unusually massive
value. I'll see if I can construct a test for this. While we could
theoretically simulate it with a minimal on-disk type whose output
function generates a 1GB string, that feels more like an
artificial/pathological case than a realistic workload.

--
Regards,
Dilip Kumar
Google

Attachment Content-Type Size
v74-0002-POC-Only-store-RI-in-conflict-log-table.patch application/octet-stream 12.6 KB
v74-0001-Implement-the-conflict-insertion-infrastructure-.patch application/octet-stream 81.4 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Sami Imseih 2026-09-17 14:53:05 Re: Reject WAIT FOR earlier in transaction-snapshot mode
Previous Message Manuel Reyes Bravo 2026-09-17 14:23:35 Re: Distinguish publication exclusions in object addresses