Summary
spock_apply_heap_insert() creates its remoteslot with a generic TTSOpsVirtual slot. That's fine for a plain insert, but when the incoming INSERT conflicts with an existing row (found=true), Spock converts it into an UPDATE and applies it via ExecSimpleRelationUpdate() using that same remoteslot — which then hits the same AM-native-slot requirement that spock_apply_heap_update() already handles correctly, but spock_apply_heap_insert() does not.
Root cause
For table AMs with no physical tuple identifier of their own — index-organized AMs such as OrioleDB, which report ROW_REF_ROWID from table_get_row_ref_type() — ExecUpdateIndexTuples() (reached from ExecSimpleRelationUpdate()) needs to retrieve the row's AM-native identifier via slot_getsysattr(RowIdAttributeNumber) on the slot being updated. A TTSOpsVirtual slot can never satisfy that. table_tuple_update() has no return-value mechanism (unlike table_tuple_insert(), which can hand back a substituted slot) for an AM to fix this up after the fact, so the slot must already be AM-native going in.
spock_apply_heap_update() (src/spock_apply_heap.c) already does this correctly:
remoteslot = table_slot_create(rel->rel, &estate->es_tupleTable);
But spock_apply_heap_insert() still does:
remoteslot = ExecInitExtraTupleSlot(estate,
RelationGetDescr(rel->rel),
&TTSOpsVirtual);
and reuses this same remoteslot in its found=true branch, which ends up in the identical ExecSimpleRelationUpdate() call as the plain-UPDATE path.
Symptom
On an INSERT/INSERT conflict (both nodes insert the same primary key concurrently) against an OrioleDB table, the conflict-resolution logic itself decides correctly (e.g. apply=1 resolution=ApplyRemote), but applying that decision then fails — observed as a crash inside the apply worker with a stray-byte/protocol-desync-looking error (unknown action of type <garbage>, see the related issue on the exception-replay path), and on retry the transaction is discarded rather than applied, leaving the two nodes permanently diverged for that row. A plain (ERROR: cannot retrieve a system column in this context / assert, depending on build) is also directly reachable through the same slot mismatch.
Reproduction
Two-node Spock cluster with an OrioleDB table on both sides, bidirectional subscriptions. Insert the same primary key concurrently from both sides (disable both subscriptions, insert different rows with the same PK on each node, then re-enable both subscriptions so the conflict is detected on apply):
-- both subscriptions disabled
-- node1
INSERT INTO oriole_items (id, name) VALUES (401, 'from-node1');
-- node2
INSERT INTO oriole_items (id, name) VALUES (401, 'from-node2');
-- re-enable both subscriptions
Suggested fix
remoteslot = table_slot_create(rel->rel, &estate->es_tupleTable);
in place of the ExecInitExtraTupleSlot(..., &TTSOpsVirtual) call in spock_apply_heap_insert(), matching the pattern already used in spock_apply_heap_update(). slot_store_data() (used to populate this slot for the plain-insert path) calls ExecStoreVirtualTuple(), which works generically regardless of the slot's underlying AM type, so this is safe for the plain-insert case too. Verified fix converges correctly across 10+ rounds of concurrent INSERT/INSERT conflicts against OrioleDB; patch available on request.
Environment
- Spock built from
main (reports as version 6.0.0)
- PostgreSQL 18.6, OrioleDB built against the same base
- Two-node bidirectional cluster
Related
Summary
spock_apply_heap_insert()creates itsremoteslotwith a genericTTSOpsVirtualslot. That's fine for a plain insert, but when the incoming INSERT conflicts with an existing row (found=true), Spock converts it into an UPDATE and applies it viaExecSimpleRelationUpdate()using that sameremoteslot— which then hits the same AM-native-slot requirement thatspock_apply_heap_update()already handles correctly, butspock_apply_heap_insert()does not.Root cause
For table AMs with no physical tuple identifier of their own — index-organized AMs such as OrioleDB, which report
ROW_REF_ROWIDfromtable_get_row_ref_type()—ExecUpdateIndexTuples()(reached fromExecSimpleRelationUpdate()) needs to retrieve the row's AM-native identifier viaslot_getsysattr(RowIdAttributeNumber)on the slot being updated. ATTSOpsVirtualslot can never satisfy that.table_tuple_update()has no return-value mechanism (unliketable_tuple_insert(), which can hand back a substituted slot) for an AM to fix this up after the fact, so the slot must already be AM-native going in.spock_apply_heap_update()(src/spock_apply_heap.c) already does this correctly:But
spock_apply_heap_insert()still does:and reuses this same
remoteslotin itsfound=truebranch, which ends up in the identicalExecSimpleRelationUpdate()call as the plain-UPDATE path.Symptom
On an INSERT/INSERT conflict (both nodes insert the same primary key concurrently) against an OrioleDB table, the conflict-resolution logic itself decides correctly (e.g.
apply=1 resolution=ApplyRemote), but applying that decision then fails — observed as a crash inside the apply worker with a stray-byte/protocol-desync-looking error (unknown action of type <garbage>, see the related issue on the exception-replay path), and on retry the transaction is discarded rather than applied, leaving the two nodes permanently diverged for that row. A plain (ERROR: cannot retrieve a system column in this context/ assert, depending on build) is also directly reachable through the same slot mismatch.Reproduction
Two-node Spock cluster with an OrioleDB table on both sides, bidirectional subscriptions. Insert the same primary key concurrently from both sides (disable both subscriptions, insert different rows with the same PK on each node, then re-enable both subscriptions so the conflict is detected on apply):
Suggested fix
in place of the
ExecInitExtraTupleSlot(..., &TTSOpsVirtual)call inspock_apply_heap_insert(), matching the pattern already used inspock_apply_heap_update().slot_store_data()(used to populate this slot for the plain-insert path) callsExecStoreVirtualTuple(), which works generically regardless of the slot's underlying AM type, so this is safe for the plain-insert case too. Verified fix converges correctly across 10+ rounds of concurrent INSERT/INSERT conflicts against OrioleDB; patch available on request.Environment
main(reports as version6.0.0)Related
table_tuple_update()on an OrioleDB table.