laskoviymishka commented on code in PR #17450:
URL: https://github.com/apache/iceberg/pull/17450#discussion_r3711442566


##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/CommitterImpl.java:
##########
@@ -192,13 +130,32 @@ public void save(Collection<SinkRecord> sinkRecords) {
       startWorker();
       worker.save(sinkRecords);
     }
+    if (reconcileNeeded) {
+      reconcileLeadership();
+      reconcileNeeded = false;
+    }
     processControlEvents();
   }
 
+  private void reconcileLeadership() {
+    Set<String> subscribedTopics = 
Sets.newTreeSet(sourceConsumer().subscription());
+    TopicPartition leader = leaderPartition(subscribedTopics);
+    if (leader != null && context.assignment().contains(leader)) {
+      startCoordinator(subscribedTopics);
+    } else {
+      stopCoordinator();
+    }
+  }
+
   private void processControlEvents() {
     if (coordinatorThread != null && coordinatorThread.isTerminated()) {
-      throw new NotRunningException(
-          String.format("Coordinator unexpectedly terminated on committer %s", 
taskId));
+      if (isProducerFenced(coordinatorThread.exception())) {
+        LOG.warn("Committer {} coordinator was fenced by a newer coordinator; 
clearing it", taskId);
+        stopCoordinator();

Review Comment:
   When we clear a fenced coordinator here, we don't re-arm `reconcileNeeded`, 
so I think we can end up with no coordinator at all.
   
   If the fence happens after the last `reconcileLeadership()` (so 
`reconcileNeeded` is already false), we null out `coordinatorThread` and 
nothing schedules another reconcile — no future `save()` re-evaluates 
leadership until the next rebalance. This task is still the leader by 
assignment, but it silently stops producing commits.
   
   Setting `reconcileNeeded = true` right after `stopCoordinator()` in this 
branch would let the next `save()` restart it. wdyt?



##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/Channel.java:
##########
@@ -53,20 +54,29 @@ abstract class Channel {
   private final Admin admin;
   private final Map<Integer, Long> controlTopicOffsets = Maps.newHashMap();
   private final String producerId;
+  private final String channelId;
 
   Channel(
       String name,
       String consumerGroupId,
       IcebergSinkConfig config,
       KafkaClientFactory clientFactory,
       SinkTaskContext context) {
+    this.channelId = config.connectorName() + "-" + config.taskId() + "-" + 
name;
     this.controlTopic = config.controlTopic();
     this.connectGroupId = config.connectGroupId();
     this.context = context;
 
-    String transactionalId = config.transactionalPrefix() + name + 
config.transactionalSuffix();
+    String transactionalId =
+        "worker".equalsIgnoreCase(name)
+            ? config.transactionalPrefix() + name + 
config.transactionalSuffix()
+            : connectGroupId + "-" + config.connectorName() + "-coord";

Review Comment:
   I'd hold on this txn-id change — it's the biggest risk in the PR for me.
   
   The format flips from `transactionalPrefix + name + transactionalSuffix` to 
`connectGroupId + "-" + connectorName + "-coord"`, so an old coordinator and a 
new one carry different transactional ids across a rolling upgrade. 
`initTransactions()` only fences the same id, so for the overlap window both 
coordinators are live — Iceberg CAS lets one win per table, but the loser can 
still advance the control-topic consumer-group offsets, and a later coordinator 
replaying from those offsets skips events.
   
   It also drops `transactionalSuffix()` (the worker path keeps it). Operators 
set that suffix to isolate producers across multiple Connect clusters, so every 
cluster's coordinator now collapses onto the same id and they fence each other.
   
   I'd keep the suffix on both paths and gate the new format behind config so 
upgrades have a migration path. (Minor while we're here: `connectGroupId` 
already defaults to `connect-<connectorName>`, so this renders as 
`connect-myconn-myconn-coord` — the name lands twice.) wdyt?



##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/CommitterImpl.java:
##########
@@ -214,11 +171,21 @@ private void startWorker() {
     }
   }
 
-  private void startCoordinator() {
+  private void startCoordinator(Set<String> subscribedTopics) {
     if (null == this.coordinatorThread) {
-      LOG.info("Task {} elected leader, starting commit coordinator", taskId);
+      int topicPartitionCount = 0;
+      for (String topic : subscribedTopics) {
+        List<PartitionInfo> partitions = sourceConsumer().partitionsFor(topic);
+        if (partitions != null) {

Review Comment:
   I think there's a data-loss path here when topic metadata isn't cached yet.
   
   `partitionsFor(topic)` returns null while the consumer hasn't fetched 
metadata for a topic (broker still loading, topic just created, first 
assignment after a cold start), and we silently skip it — so 
`topicPartitionCount` can come out 0 and the Coordinator is built expecting 0 
partitions. With nothing to wait for, the first `DATA_COMPLETE` looks 
commit-ready and we run a full commit while the other tasks' files are still 
buffered, and those get dropped on `clearResponses`.
   
   The old code summed partitions from the group members, which was 
authoritative. I'd either restore an authoritative count or treat `count == 0` 
as unknown and never take the full-commit path in that state — a timeout-based 
partial commit is fine there, dropping files isn't. Could we guard that case?



##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/Coordinator.java:
##########
@@ -195,6 +193,14 @@ private void commit(boolean partialCommit) {
     }
   }
 
+  @VisibleForTesting
+  static boolean isRetryable(RuntimeException exception) {
+    return exception instanceof CommitFailedException
+        || exception instanceof 
org.apache.kafka.clients.consumer.CommitFailedException
+        || exception instanceof 
org.apache.kafka.common.errors.RebalanceInProgressException
+        || exception instanceof 
org.apache.kafka.common.errors.RetriableException;

Review Comment:
   Broadening retry to the whole `RetriableException` hierarchy is where I'd 
want to be surgical.
   
   Iceberg's `CommitFailedException`, the Kafka consumer 
`CommitFailedException`, and `RebalanceInProgressException` are all safe to 
retry. But a `TimeoutException` out of `producer.commitTransaction()` is 
ambiguous — per the Kafka docs the broker may already have committed — so 
retrying by starting a fresh transaction and committing again risks a double 
write. Config-type `RetriableException`s like `NotEnoughReplicas*` will also 
retry to the threshold while logging "will retry", which just misleads 
operators.
   
   I'd enumerate the exceptions we actually treat as transient for a 
transactional commit rather than catch the superclass. Which ones did you 
intend to be retryable here?



##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/CommitterImpl.java:
##########
@@ -192,13 +130,32 @@ public void save(Collection<SinkRecord> sinkRecords) {
       startWorker();
       worker.save(sinkRecords);
     }
+    if (reconcileNeeded) {
+      reconcileLeadership();
+      reconcileNeeded = false;
+    }
     processControlEvents();
   }
 
+  private void reconcileLeadership() {
+    Set<String> subscribedTopics = 
Sets.newTreeSet(sourceConsumer().subscription());

Review Comment:
   This election looks deterministic for a static `topics` list but I think it 
breaks for `topics.regex`.
   
   `subscription()` returns only the topics this task instance currently 
resolved/was assigned, not the full cluster set. With a static list every task 
sees the same set, so `leaderPartition` agrees. With a regex, task A might see 
`[event-aaa, event-bbb]` and task B `[event-ccc, ...]`, so they compute 
different local-min topics and each can elect itself → two coordinators 
overlapping, or zero if nobody holds partition 0 of their local min.
   
   Election needs a global view of the subscribed topics for the regex case. 
Could we resolve it from cluster state, or explicitly reject/document regex 
here for now?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to