Re: Proposal: Conflict log history table for Logical Replication

From: shveta malik <shveta(dot)malik(at)gmail(dot)com>
To: Dilip Kumar <dilipbalaut(at)gmail(dot)com>
Cc: 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>, 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>, 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>, shveta malik <shveta(dot)malik(at)gmail(dot)com>
Subject: Re: Proposal: Conflict log history table for Logical Replication
Date: 2026-09-15 06:53:15
Message-ID: CAJpy0uA02miWt31kgE9wcS0mt_uE1Q+HNXNiavwcc_AjMeEetw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Wed, Sep 9, 2026 at 9:11 PM 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.
>
> 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.
>
> 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.
>
> 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).
>
> 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.
>
> In summary, only Options 3 and 4 are viable; while the first two
> options reduce the error surface, they do not eliminate it entirely.

Thanks for the detailed analysis, Dilip. I gave it some thought. If we
plan to go with Option 4 (assuming the subtransaction overhead is
acceptable), then another approach worth considering would be to
combine Options 1 and 4.

Even though Option 4 acts as a complete safety net and protects
against failures in typoutput, UDTs, etc., it may still bloat the
table. If a few high-volume conflicts log 500 MB to 1 GB of JSON text
each, it will rapidly fill up disk space and bloat the CLT.

We can combine Option 1 and Option 4: limit attributes to 16KB via
Option 1, and use Option 4 for any UDT-related or other such issues.
Option 4 can catch errors and convert them directly to an omitted
marker. That way, the worker never crashes and the CLT doesn't bloat.

thanks
Shveta

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Michael Paquier 2026-09-15 07:08:04 Re: Support for 8-byte TOAST values, round two
Previous Message jian he 2026-09-15 06:50:49 Re: bug, ALTER TABLE call ATPostAlterTypeCleanup twice for the same relation