Kafka Connect: Track control topic offsets as a high-water mark - #17933
Kafka Connect: Track control topic offsets as a high-water mark#17933vbhanuchander-lang wants to merge 1 commit into
Conversation
Channel.consumeAvailable recorded the consumed position with an unconditional put, so re-reading a control topic partition moved the tracked offset backwards. commitConsumerOffsets then commits the regressed value for the consumer group, and the coordinator stamps it onto the snapshot as kafka.connect.offsets, which makes the regression durable: a later restart resumes behind records that were already handled, and the replayed envelopes pass the min-offset filter in commitToTable, so their data files are committed a second time. Track the furthest position consumed instead, which is what every reader of controlTopicOffsets() already assumes.
uros-b
left a comment
There was a problem hiding this comment.
Looks like a correct fix, so thank you @vbhanuchander-lang! Should definitely ping relevant committers for further review, e.g. @bryanck who created the IKC sink and commit-coordination path or @danielcweeks
|
Thanks @uros-b — and you pointed at the right person: #10351 ("Kafka Connect: Commit coordination") @bryanck @danielcweeks a short summary so this is quick to judge, since it is a one-line change with
The argument I would most like checked:
One open question for you rather than for the diff: #17340 also floats a bounded set of recently |
| // partition: a re-read of the control topic, e.g. after a rebalance resumes from the | ||
| // last committed offsets, would otherwise move the tracked position backwards and | ||
| // commit a consumer offset behind records that were already handled. | ||
| controlTopicOffsets.merge(record.partition(), record.offset() + 1, Long::max); |
There was a problem hiding this comment.
Answered in the timeline — short version: different method, different cause, and each PR's tests fail against the other's fix.
|
@danielcweeks @twthorn They overlap in symptom but not in cause, and neither one subsumes the other — I checked by running each PR's tests against the other's fix. #17552 guards the write: a per-instance Applying #17552's #17552's guard does not fire here because the cache is empty on an instance's first commit — The reverse also holds. This PR's Making one instance's map monotonic says nothing about another coordinator's position. One further gap: The two are complementary and do not conflict textually — different methods in the same file. #17552 is further along, so I am happy to rebase this on top of it once it lands. |
Closes #17340.
Opening this at @ericyangliu's invitation on the issue — the diagnosis and the production evidence
(149 double-referenced files, ~112k duplicated rows) are his.
The bug
Channel.consumeAvailablerecorded the consumed position with an unconditionalput:Nothing compares against the value already stored, so any re-read of a control topic partition —
a rebalance resuming the consumer from the last committed group offsets, as in the report — moves
the tracked position backwards.
That regression is durable rather than transient, because the map is not just bookkeeping:
commitConsumerOffsets()commits it for the consumer group, so the next restart resumes from theregressed offset and re-reads more.
Coordinator.commitToTablemerges it intokafka.connect.offsetson the snapshot, which is thewatermark the min-offset filter uses on subsequent commits.
Once the watermark is behind, replayed
DataWrittenenvelopes pass that filter.distinctByKeyonly dedupes within one commit and append does no path-level dedup, so the same data files are
committed again and every scan reads them twice.
The change
One line: keep the highest position seen for the partition.
This is what every reader of
controlTopicOffsets()already assumes —commitToTableeven foldsit in with
Long::maxagainst the last committed offsets. Making the map itself monotonic isconsistent with that, and it does not change the offsets recorded on the forward path.
Tests
TestChanneldrives aChannelover aMockConsumer. Consuming offsets 0-4 reaches a watermarkof 5; a seek back to 1 then delivers a partial replay ending at offset 2, which is the shape of the
re-read in the report.
controlTopicOffsetsTrackTheHighestPositionConsumed— the map stays at 5. With the fix revertedit is 3.
committedControlTopicOffsetsDoNotRegressOnReplay— asserts what the channel actually commits toKafka,
OffsetAndMetadata{offset=5}. With the fix reverted it commits 3, which is the offset arestarted channel would resume from.
controlTopicOffsetsAreTrackedPerPartition— partitions stay independent.Both regression tests fail on
mainwithout the change. Full module suite passes (136 tests), asdo
spotlessCheckand checkstyle.Not included
The issue also floats a bounded set of recently committed file locations in the coordinator as a
content-level backstop for replays the offset arithmetic cannot see. That is a design call for
maintainers and a larger change, so I have left it out rather than hold up the correctness fix.
Happy to follow up if it is wanted.