jorgebo10 opened a new issue, #17637:
URL: https://github.com/apache/iceberg/issues/17637

   ### Apache Iceberg version
   
   1.11.0 (latest release)
   
   ### Query engine
   
   Kafka Connect
   
   ### Please describe the bug 🐞
   
   A single rebalance can leave two `CoordinatorThread`s alive for one 
connector. Both keep their own commit timer and both broadcast `StartCommit` on 
the control topic, which workers cannot distinguish from each other.
   
   #### Symptom
   
   Observed in production on a sink with 10 tasks: the connector's coordinator 
consumer group `connect-<connector>-coord` had **two** members instead of the 
expected one, and stayed in `COMPLETING_REBALANCING` across repeated samples 
60s apart. All 10 worker control groups (`cg-control-*`) were alive and stable 
at the same time.
   
   When leadership cannot be resolved, the closing task logs:
   
   ```
   WARN  o.a.i.connect.channel.CommitterImpl - Committer <connector>-0 found no 
partitions assigned
         across all members, cannot determine leader
   ```
   
   #### Root cause
   
   `CommitterImpl.close()` decides whether to stop its coordinator by 
re-deriving leadership from a live `Admin` describe of the connect consumer 
group (line numbers on current `main`):
   
   ```java
   // CommitterImpl.java:179
   if (hasLeaderPartition(closedPartitions)) {
     LOG.info("Committer {} lost leader partition. Stopping coordinator.", 
taskId);
     stopCoordinator();
   }
   ```
   
   ```java
   // CommitterImpl.java:78
   boolean hasLeaderPartition(Collection<TopicPartition> 
currentAssignedPartitions) {
     ConsumerGroupDescription groupDesc;
     try (Admin admin = clientFactory.createAdmin()) {
       groupDesc = KafkaUtils.consumerGroupDescription(config.connectGroupId(), 
admin);
     }
     ...
   }
   ```
   
   But `close()` runs *during* the rebalance that is revoking the partitions. 
At that point the group is not stable, and `describeConsumerGroups` can return 
members that carry no assignments at all. `findFirstTopicPartition()` then 
returns `null`, `containsFirstPartition()` logs the warning above and returns 
`false`, and `stopCoordinator()` is never called.
   
   The next `open()` on another task sees a settled group, finds it owns the 
lowest partition, and starts a coordinator of its own — `startCoordinator()`'s 
`coordinatorThread == null` guard is per-instance and cannot see the other 
task's thread. The previous coordinator is now orphaned: nothing holds a 
reference that would ever stop it.
   
   The decision is also redundant. A task that owns a coordinator already knows 
it locally — `coordinatorThread != null` means "I started one and never stopped 
it". `close()` overrides that fact with an inference drawn from data that is 
specifically unreliable at that instant, and the inference's failure mode ("I 
cannot tell") resolves to "do nothing".
   
   #### Reproduction
   
   Deterministic, in-process, no broker required — simulate one rebalance 
across two committers and count live `iceberg-coord` threads:
   
   1. Group is stable; task A owns the lowest partition, `open()` elects it and 
starts one coordinator.
   2. Group description switches to members with empty assignments (the 
mid-rebalance state).
   3. `taskA.close([lowest partition])` — returns without stopping the 
coordinator.
   4. Group description settles again; `taskB.open([lowest partition])` elects 
B and starts a second coordinator.
   
   ```
   java.lang.AssertionError: after one rebalance there must still be exactly 
one live iceberg-coord thread
   Expected size: 1 but was: 2 in:
   [Thread[#36,iceberg-coord,5,main], Thread[#35,iceberg-coord,5,main]]
   ```
   
   #### Suggested fix
   
   Stop asking the broker on the revocation path. Record the partition 
leadership was won with at `open()` time, and have `close()` compare it against 
the revoked partitions locally:
   
   ```java
   private TopicPartition leaderPartition;   // set in hasLeaderPartition() 
when the check passes
   
   // close()
   if (coordinatorThread != null
       && (leaderPartition == null || 
closedPartitions.contains(leaderPartition))) {
     stopCoordinator();
   }
   ```
   
   Checking the *specific* elected partition rather than "did I lose anything" 
keeps this correct under cooperative rebalancing, where a task can be closed 
for a subset of its partitions and legitimately keep leadership. Falling back 
to stopping when the elected partition is unknown is the safe direction, since 
an orphaned coordinator corrupts data while a missing one is re-elected on the 
next `open()`.
   
   I have a branch with the fix and the test above, and will open a PR 
referencing this issue.
   
   #### Related but distinct
   
   - #16016 — same symptom class (a coordinator thread that outlives the task) 
but a different trigger and mechanism: a write failure kills the task, and 
`CoordinatorThread.terminate()` does not join the thread, so the coordinator 
keeps running against a catalog that `IcebergSinkTask.close()` has already 
closed. Not addressed by this fix.
   - #17340 — duplicate data files, but attributed there to 
`Channel.consumeAvailable` regressing the control-topic watermark on re-read, 
which is independent of coordinator count.
   
   ### Willingness to contribute
   
   - [x] I can contribute a fix for this bug independently
   - [ ] I would be willing to contribute a fix for this bug with guidance from 
the Iceberg community
   - [ ] I cannot contribute a fix for this bug at this time
   


-- 
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