Skip to content

Consumer deduplication guard is never invoked because ShapeLogCollector already guards against this particular case #3849

Description

@alco

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/3Storage.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:

  1. state.latest_offset is set to the log offset of the last matching change (line 586), not to txn.last_log_offset.
  2. 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}.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions