Skip to content

Probably you need to know PG logical replication is broken a bit #599

Description

@michail-nikolaev

https://commitfest.postgresql.org/patch/5151/

This is AI-build report below

Is pgEdge/spock affected by CF #5151 (DirtySnapshot index scan skips concurrently updated tuples)?

Yes — in three places. Two of them are fixed for free once the core patch lands; one is a
private copy inside spock that the core fix cannot reach.

Analyzed: pgEdge/spock master @ 7f42238, SPOCK_VERSION 6.0.0.

1. Direct inheritance (fixed by the core patch)

FindReplTupleInLocalRel() (src/spock_apply_heap.c:394) calls exactly the two functions the
patch rewrites to use GetLatestSnapshot():

src/spock_apply_heap.c:417   RelationFindReplTupleByIndex(localrel, localidxoid, ...)
src/spock_apply_heap.c:431   RelationFindReplTupleSeq(localrel, LockTupleExclusive, ...)

It is reached from all three apply paths: INSERT (:878), UPDATE (:984), DELETE (:1101).
No spock change needed — but only on minor releases that carry the core fix.

2. Symptoms differ from vanilla logical replication, and are partly worse

The "tuple not found" branch behaves differently per operation:

  • UPDATE — elog(ERROR, "logical replication did not find row to be updated ...")
    (src/spock_apply_heap.c:1038) plus SPOCK_CT_UPDATE_MISSING in the exception log.
    Not a silently lost update: a sporadic, unexplainable apply failure on a hot row
    (worker behaviour then depends on spock.exception_behaviour).
  • DELETE — spock_report_conflict(SPOCK_CT_DELETE_MISSING, ...) (src/spock_apply_heap.c:1210);
    the delete is silently dropped. Exactly the "lost delete" + wrong-conflict-type case
    from the thread.
  • INSERT — the existing row is not found, so ExecSimpleRelationInsert() runs and raises a
    duplicate key violation instead of resolving an insert_exists conflict. For a
    multi-master deployment this breaks the very feature spock is deployed for.

3. Private copy the core patch will NOT fix

SpockRelationFindReplTupleByIndex() — src/spock_common.c:333. The only InitDirtySnapshot()
in the whole tree (src/spock_common.c:394), followed by the same
index_beginscan / index_getnext_slot / XactLockTableWait + goto retry pattern as the
pre-patch core function.

Caller: FindReplTupleByUCIndex() (src/spock_apply_heap.c:471) — INSERT conflict lookup across
all unique indexes other than PK/RI, gated by spock.check_all_uc_indexes (off by default).

The same fix applies. The thread's "no performance regression" argument holds literally here:
PushActiveSnapshot(GetLatestSnapshot()) already sits a few lines below, before
table_tuple_lock (src/spock_common.c:442).

4. Existing mitigation and its limits

UPDATE and DELETE wrap the lookup in a retry loop, spock.read_retry_count, default 5
(src/spock.c:179). Each retry re-scans with a fresh dirty snapshot, so once the concurrent
writer has committed the row is found — the window is materially narrower than in vanilla.
It is not a fix:

  • the INSERT path has no retry at all;
  • wait_for_previous_transaction() (src/spock_apply.c:294) waits for the previous transaction
    of the same origin by commit timestamp, not for the local concurrent writer — so these are
    effectively five back-to-back re-scans with no delay, and a continuously updated row can lose
    all five to the same race;
  • the GUC minimum is 0.

5. Not affected

The companion thread's check_exclusion_or_unique_constraint path does not apply: spock never
calls ExecCheckIndexConstraints and does not use speculative insertion. Its INSERTs go through
ExecInsertIndexTuples / _bt_check_unique, which is not a dirty-snapshot scan.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions