Skip to content

unknown action of type <char> exception-replay crash still reproducible on main (possible regression/incomplete fix of #40) #610

Description

@mrayva

Summary

unknown action of type <char> crashes are still reproducible on current main (reports as Spock 6.0.0), under spock.exception_behaviour = transdiscard (the default), when an update_missing conflict occurs (one node deletes a row while the other concurrently updates it). This looks like the same failure reported in #40, which a maintainer said was resolved in 5.0.0 via #42's replay-queue rewrite (ApplyReplayContext / apply_replay_queue_*) — but I can reproduce the identical symptom against that current implementation.

Reproduction

Two-node bidirectional Spock cluster (reproduced against an OrioleDB table; see note on heap tables below). With both subscriptions active, fire a genuinely concurrent UPDATE (node1) / DELETE (node2) on the same row:

-- node1
UPDATE items SET name='updated-by-node1' WHERE id=700;
-- node2, committed close enough in time to race node1's replicated change
DELETE FROM items WHERE id=700;

Observed apply-worker log on the node receiving the now-missing row's UPDATE:

LOG:  spock_apply_heap_update() retried 5 times
LOG:  SPOCK: caught initial exception - logical replication did not find row to be updated in replication target relation (public.items)
LOG:  SPOCK sub_x_y: error during exception handling: SPOCK sub_x_y: unknown action of type o
LOG:  SPOCK sub_x_y: exiting to allow worker restart
ERROR:  SPOCK sub_x_y: unknown action of type o
CONTEXT:  apply (unknown action) in commit before 0/1D650DE, xid 64 committed at ... (action #2) from node replorigin ...
LOG:  apply worker [...] exiting with error

The garbage action byte is not stable across runs — I observed both 'o' (0x6F) and 'B' (0x42, which is otherwise a valid action letter — BEGIN) in different runs, which points at reading stale/reused memory in the replay path rather than a fixed off-by-one. After the worker restarts and reprocesses the same LSN, it takes a different branch entirely and discards the transaction outright:

LOG:  SPOCK sub_x_y: Transaction discarded in TRANSDISCARD mode at LSN ... Initial error: logical replication did not find row to be updated ...

— i.e. the UPDATE is never applied on that node, and the two nodes are left permanently diverged for that row (the "losing" node keeps its own local delete instead of picking up the winning node's update).

Note on OrioleDB vs. heap tables

I could reliably reproduce this against an OrioleDB table, but the identical test against a plain heap-table Spock cluster (same topology, same conflict pattern) did not crash — it went straight to a clean Transaction discarded in TRANSDISCARD mode. The one consistent difference in the logs is that the OrioleDB run always shows spock_apply_heap_update() retried 5 times (i.e. FindReplTupleInLocalRel()'s wait_for_previous_transaction() retry loop runs to exhaustion before concluding the row is genuinely missing) immediately before the crash, whereas the heap-table run has no such retry line. My working hypothesis is that OrioleDB's slower/different local-tuple-visibility characteristics widen a narrow, pre-existing race in the replay-queue lifecycle (around ApplyReplayContext / apply_replay_queue_reset() / the transition into use_try_block replay via goto stream_replay) that's rarely hit against heap tables' near-instant missing-row detection. I have not fully root-caused the race itself — I looked at apply_replay_queue_append_entry() / apply_replay_queue_next_entry() / apply_replay_queue_start_replay() without finding an obvious bug in isolation, so the corruption is likely coming from a memory-context lifetime issue (MemoryContextReset(MessageContext) / MemoryContextReset(ApplyOperationContext) around the "caught initial exception" handling in apply_work(), or ApplyReplayContext being reset while still referenced) rather than the append/read logic itself.

Why I think this is worth a fresh look rather than assuming #40 covers it

Environment

  • Spock built from main (reports as version 6.0.0)
  • PostgreSQL 18.6
  • Reproduced with OrioleDB tables (orioledb.so + spock.so both in shared_preload_libraries); not reproduced (in limited testing) against heap tables under the same scenario
  • spock.exception_behaviour = transdiscard (default)

Related

I don't have a fix for this one — happy to help narrow it down further (e.g. with a debug build and gdb) if that'd be useful, but wanted to get it filed with what I have so far.

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