On Wed, Sep 9, 2026 at 9:11 PM Dilip Kumar <[email protected]> wrote:
>
> On Tue, Sep 1, 2026 at 4:03 PM shveta malik <[email protected]> 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


Reply via email to