Kafka Connect: Rework commit-coordinator election (drop Admin describeConsumerGroups) and harden coordinator fencing, recovery, and shutdown. - #17376
Conversation
|
@bryanck can you please take a look into this. |
|
@laskoviymishka can you please check this one. |
| // the consumer stores the offsets that corresponds to the next record to consume, | ||
| // so increment the record offset by one | ||
| controlTopicOffsets.put(record.partition(), record.offset() + 1); | ||
| controlTopicOffsets.merge(record.partition(), record.offset() + 1, Long::max); |
There was a problem hiding this comment.
Alternative to the put() → merge(..., Long::max) line, in case it's useful — handling the rewind
in a ConsumerRebalanceListener so the replay never happens:
@Override public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
for (TopicPartition partition : partitions) {
Long inMemoryOffset = controlTopicOffsets.get(partition.partition());
if (inMemoryOffset != null && inMemoryOffset > consumer.position(partition)) {
consumer.seek(partition, inMemoryOffset); // + a warn
}
}
}merge(Long::max) stops the duplicate files, but the replay still happens: the replayed
DataComplete double-count in readyBuffer, so the commit still fires early and validThroughTs()
takes min(timestamp) over only the partitions that reported — the snapshot can claim a
valid-through-ts later than the true watermark. Seeking forward avoids that, and is safe because
the skipped records were consumed by this same Channel and are already in commitBuffer.
Context on #16282. Branch on top of 1.11.0 with regression tests:
apache-iceberg-1.11.0...emlynazuma:iceberg:tongwai/ikc-1.11.0-rebalance-fixes
There was a problem hiding this comment.
@emlynazuma I am adding a proper rebalance consumer listener but I do not think it will be needed if the zombie coordinator scenario is properly handled which after the deterministic changes that I have made will make it almost impossible to have two coordinators running. But to be on the safer side I will add a proper rebalance listener. Also I think the best place to clear thinks are in the revoked partitions. As revoked path is called first if there is any think to revoke so we can just clear the Data Written, Data Complete and control topic offsets stored against those partitions. But I was just analyzing for it to not become complicated. Let me know about your thoughts on these.
There was a problem hiding this comment.
I agree what you have suggested is simpler and more clean. Incorporated that change in the PR. Thanks.
There was a problem hiding this comment.
Looks like this landed in onPartitionsAssigned via seekToTrackedOffsets rather than clearing on
revoke — matches what I raised above about revoke-time clearing risking dropping buffered records
that haven't committed yet. And it's still doing work independent of the fencing: open()/close()
starting and stopping the coordinator is itself a -coord membership change, so the rewind can still
happen on a clean single-coordinator handoff, not just during a split-brain window.
Will test this against our repro setup and report back.
There was a problem hiding this comment.
Yes @emlynazuma what you suggested looked clean and sufficient to handle seeking part. Also preventing zombie coordinator in the open and close is much more important than this as that is the reason which is causing this. Also I was thinking that rather than doing all this mumbo-zumbo can't we just do this:
protected void consumeAvailable(Duration pollDuration) {
ConsumerRecords<String, byte[]> records = consumer.poll(pollDuration);
while (!records.isEmpty()) {
for (ConsumerRecord<String, byte[]> record : records) {
// the consumer stores the offset of the next record to consume,
// so increment the record offset by one
if (record.offset() < controlTopicOffsets.getOrDefault(record.partition(), 0L)) {
LOG.warn("Channel {} processed an already processed offset {} for partition {}", taskId, record.offset(), record.partition());
continue;
}
controlTopicOffsets.put(record.partition(), record.offset() + 1);
Event event = AvroUtil.decode(record.value());
if (event.groupId().equals(connectGroupId)) {
LOG.debug("Received event of type: {}", event.type().name());
if (receive(new Envelope(event, record.partition(), record.offset()))) {
LOG.info("Handled event of type: {}", event.type().name());
}
}
}
records = consumer.poll(pollDuration);
}
}
This along with the open and close fix and ensuring closed coordinator in the committer should be sufficient I guess. What do you think?
There was a problem hiding this comment.
I will make this change and test if this is suffcient as this will simplyfy and minimize the changes.
… the issues without affecting the save path
What
Reworks how the Iceberg Kafka Connect sink elects its single commit
Coordinatorand hardens the coordinator's fencing, recovery, and shutdown paths.Election — no Admin API call
Admin.describeConsumerGroups(connectGroupId)on every rebalance and picking the task that owned the globally-lowest(topic, partition)from a transient, mid-rebalance group snapshot. This required aDESCRIBEACL, added an Admin round-trip to the rebalance path, and was racy during cooperative rebalancing.subscription()— connector-wide, identical across tasks, andvalid for both a literal
topicslist and atopics.regex. No Admin call. Leadership is evaluated in theopen/closerebalance callbacks: a task that gains the leader partitionstarts the coordinator; a task that loses it stops it.
partitionsFor(...)over the subscribed topics, instead of a one-time member-assignmentsnapshot.
Coordinator hardening
transactional.id. The coordinator producer id is now<prefix>coordinator-<connectorName>— identical across a connector's tasks and stable acrossrestarts/reconfigurations — so a newly elected coordinator's
initTransactions()epoch-bumps and fences a prior (zombie) coordinator's control-plane writes. Worker ids stay per-task(
<prefix><taskId>-worker-<suffix>) so sibling workers never fence each other.ProducerFencedException/InvalidProducerEpochException/UnknownProducerIdException, matched across thecause chain) is cleared without failing the task (Connect does not auto-restart FAILED tasks); any other coordinator termination still fails the task loudly.
earliest. The coordinator's-coordconsumer group now defaults toauto.offset.reset=earliest, so a fresh or expired group re-reads uncommitted controlevents instead of skipping to the log end and dropping their data. Replay is idempotent via the snapshot offset floor +
distinctByKey(location)dedup + the offsets compare-and-swap.Channelis now its ownConsumerRebalanceListener; on (re)assignment the coordinator seeks each control-topic partition forward to the offset ithas already consumed, so an eager rebalance that rewinds the fetch position does not re-read buffered events. The tracked control offset is merged with
Long::max, so it neverregresses.
commitConsumerOffsets()(KafkaCommitFailedException/RebalanceInProgressException/RetriableException, e.g. the-coordconsumer briefly evicted when a catalog commit exceedsmax.poll.interval.ms) no longer fails the task — the table commit already succeeded andthe watermark re-advances next cycle; a persistent failure still terminates after
commitMaxConsecutiveFailures.stopCoordinatorclears state first, then signals termination and waits (bounded) for the thread to release its producer/consumer/admin beforereturning; a
terminate()failure is best-effort (logged, not fatal) and interrupts are preserved (Coordinator.terminate()restores the interrupt flag).KafkaClientFactory.createProducercloses the producer ifinitTransactions()throws.API
Committer.configure(Catalog, IcebergSinkConfig, SinkTaskContext)lifecycle hook (invoked fromIcebergSinkTask.start) for one-time setup. The deprecatedstart/stopmethods are unchanged.Compatibility
transactional.idadds control-plane fencing on top.offsets-CAS-safe.
Testing
open/closestart/stop of the coordinator, fenced-vs-fatal coordinator termination (including wrapped,InvalidProducerEpoch, andUnknownProducerIdcauses), the retryable-commit classification, seek-forward-on-rebalance, monotonic control-offset merge, the transactional-id format, andbest-effort
stopCoordinator.