Monthly Summary: Preserve Replication Origin OIDs in pg_upgrade (May 2026)
Problem Statement
When pg_commit_ts SLRU data is migrated byte-for-byte during pg_upgrade, the embedded 2-byte RepOriginId values become semantically incorrect if replication origin OIDs are not preserved. This causes spurious update_origin_differs / delete_origin_differs conflicts after upgrading a logical replication subscriber, and in the worst case attributes row modifications to the wrong upstream publisher.
The root cause is a three-way mismatch: pg_upgrade historically doesn't preserve subscription OIDs → origin names (pg_<suboid>) change → replorigin_create() assigns fresh roidents in unpredictable order → SLRU records reference stale roident values.
Design Evolution
v1–v2: Special-case subscription origins
Ajin's initial approach treated subscription-associated origins differently from user-created ones, using separate code paths and special pg_dump/pg_dumpall support functions.
v3: Unified approach via subscription OID preservation
After Kuroda observed that preserving subscription OIDs would stabilize origin names, and Shveta correctly noted that name stability alone doesn't guarantee roident stability, the design converged on:
- Patch 0001 (Vignesh): Preserve subscription OIDs through pg_upgrade using existing
binary_upgrade_set_next_*_oidmachinery - Patch 0002 (Ajin): Uniformly preserve all replication origins (subscription-associated and user-created) with their
(roname, roident, remote_lsn)triples
v4–v5: Implementation hardening
Addressed review findings including silent OID truncation (uint16 vs Oid mismatch), dead code removal, redundant lock cycling (eliminated replorigin_create_with_reploriginid entirely by inlining), and parallel safety annotations.
v6: Current state under review
Remaining issues identified but not yet resolved in a new version.
Key Design Decisions
-
Backward compatibility from PG17+ (not PG19+): Ajin argues that restricting to PG19+ would leave subscriptions without origins when upgrading from PG17/18, since the new code suppresses
CREATE SUBSCRIPTION's normal origin creation. -
Direct arguments over global-variable convention: Since
binary_upgrade_create_replication_originis itself the creation call (not a side-channel beforeCREATE), passing(roname, roident)directly is cleaner than the set-global-then-create pattern used for tables/types. -
Single-function architecture: v5 eliminated the intermediate
replorigin_create_with_reploriginidfunction, consolidating all work into one function with one lock acquisition.
Outstanding Bugs (as of month-end)
-
Origin name truncation (Shveta): Function declares
NAMEtype (64-byte fixed) for the origin name parameter, butpg_replication_origin.ronameisTEXT(unbounded). Origins with names >63 characters are silently truncated during upgrade. Fix: change parameter toTEXTtype. -
Pre-existing origins on target cluster (Shlok): If the new cluster already has replication origins, upgrade fails late (during "Performing Upgrade") with a hard error. Needs a pre-flight consistency check during the early validation phase.
-
Minor cleanup: Unnecessary includes, unexplained Assert on TOAST relid, and insufficient code comments explaining the PG17 version gate.
Month Trajectory
The thread progressed rapidly from problem identification through three major revision cycles. The architectural design was settled by mid-month (v3), with subsequent work focused on implementation correctness. Two significant bugs remain (name truncation, missing pre-flight check) requiring a v7. The patch is approaching committable shape but needs one more revision cycle.