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.
Summary
unknown action of type <char>crashes are still reproducible on currentmain(reports as Spock6.0.0), underspock.exception_behaviour = transdiscard(the default), when anupdate_missingconflict 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:
Observed apply-worker log on the node receiving the now-missing row's UPDATE:
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:— 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 showsspock_apply_heap_update() retried 5 times(i.e.FindReplTupleInLocalRel()'swait_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 (aroundApplyReplayContext/apply_replay_queue_reset()/ the transition intouse_try_blockreplay viagoto 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 atapply_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 inapply_work(), orApplyReplayContextbeing 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
main, well after that fix landed, using the sameexception_behaviour=transdiscard/update_missingscenario as Replication failing to delete a non existing row #40.Environment
main(reports as version6.0.0)orioledb.so+spock.soboth inshared_preload_libraries); not reproduced (in limited testing) against heap tables under the same scenariospock.exception_behaviour = transdiscard(default)Related
unknown action+update_missing/transdiscardsymptom, on Spock 4.0.10, said to be resolved in 5.0.0.commit_lsnmarker after a connection-class failure), fixed after 5.0.10.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.