Problem
The deduplication guard in Shapes.Consumer.do_handle_txn/2 is dead code:
|
defp do_handle_txn(%Transaction{} = txn, state) |
|
when LogOffset.is_log_offset_lte(txn.last_log_offset, state.latest_offset) do |
|
Logger.debug(fn -> "Skipping already processed txn #{txn.xid}" end) |
|
|
|
{:cont, consider_flushed(state, txn)} |
|
end |
Why it's dead code
All transaction fragments flow through the ShapeLogCollector before reaching any consumer. The ShapeLogCollector has its own deduplication guard (shape_log_collector.ex:471-484) that correctly compares txn_fragment.last_log_offset against last_processed_offset and drops duplicates before they are dispatched to consumers.
When a consumer process restarts, it repopulates state.latest_offset from storage (via Consumer.State.initialize/3 → Storage.fetch_latest_offset), which is guaranteed to be a transaction boundary offset by PureFileStorage. But the consumer doesn't independently re-receive fragments — the ShapeLogCollector is the sole dispatcher, and it tracks what's already been processed.
There is no code path where a duplicate transaction bypasses the ShapeLogCollector and reaches the consumer directly.
Why the guard is also incorrect
Even if the guard weren't dead code, it would fail to detect replayed transaction fragments when the last change in the fragment doesn't match the shape:
state.latest_offset is set to the log offset of the last matching change (line 586), not to txn.last_log_offset.
- A transaction fragment with changes at offsets
[{lsn, 0}, {lsn, 1}, {lsn, 2}] where only {lsn, 0} matches the shape would set state.latest_offset = {lsn, 0}.
- On replay, the guard checks
{lsn, 2} <= {lsn, 0} → false, so the fragment would be re-processed.
state.latest_offset can't simply be changed to store txn.last_log_offset because it's also used in handle_call({:subscribe_materializer, ...}) (line 216) as an upper bound for reading the log stream from storage. That value must correspond to an actual offset written to the log.
Suggested fix
Remove the dead code guard entirely. If consumer-level deduplication is desired in the future (e.g. as a defense-in-depth measure), it would need a separate field like latest_txn_fragment_offset that stores the last_log_offset of the most recently processed transaction fragment regardless of whether any changes matched the shape.
Problem
The deduplication guard in
Shapes.Consumer.do_handle_txn/2is dead code:electric/packages/sync-service/lib/electric/shapes/consumer.ex
Lines 499 to 504 in e1dd7ef
Why it's dead code
All transaction fragments flow through the
ShapeLogCollectorbefore reaching any consumer. The ShapeLogCollector has its own deduplication guard (shape_log_collector.ex:471-484) that correctly comparestxn_fragment.last_log_offsetagainstlast_processed_offsetand drops duplicates before they are dispatched to consumers.When a consumer process restarts, it repopulates
state.latest_offsetfrom storage (viaConsumer.State.initialize/3→Storage.fetch_latest_offset), which is guaranteed to be a transaction boundary offset byPureFileStorage. But the consumer doesn't independently re-receive fragments — the ShapeLogCollector is the sole dispatcher, and it tracks what's already been processed.There is no code path where a duplicate transaction bypasses the ShapeLogCollector and reaches the consumer directly.
Why the guard is also incorrect
Even if the guard weren't dead code, it would fail to detect replayed transaction fragments when the last change in the fragment doesn't match the shape:
state.latest_offsetis set to the log offset of the last matching change (line 586), not totxn.last_log_offset.[{lsn, 0}, {lsn, 1}, {lsn, 2}]where only{lsn, 0}matches the shape would setstate.latest_offset = {lsn, 0}.{lsn, 2} <= {lsn, 0}→ false, so the fragment would be re-processed.state.latest_offsetcan't simply be changed to storetxn.last_log_offsetbecause it's also used inhandle_call({:subscribe_materializer, ...})(line 216) as an upper bound for reading the log stream from storage. That value must correspond to an actual offset written to the log.Suggested fix
Remove the dead code guard entirely. If consumer-level deduplication is desired in the future (e.g. as a defense-in-depth measure), it would need a separate field like
latest_txn_fragment_offsetthat stores thelast_log_offsetof the most recently processed transaction fragment regardless of whether any changes matched the shape.