twthorn commented on code in PR #17552:
URL: https://github.com/apache/iceberg/pull/17552#discussion_r3787029815


##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/Channel.java:
##########
@@ -142,13 +143,35 @@ protected Map<Integer, Long> controlTopicOffsets() {
     return controlTopicOffsets;
   }
 
+  /**
+   * Commit consumer offsets. Only commits offsets if it has not committed 
offsets before or the
+   * value is greater than the cached offset.
+   *
+   * <p>Note: there is a risk that two parallel coordinators may overwrite 
each other's offsets, to
+   * be fixed in a follow-up PR.
+   */
   protected void commitConsumerOffsets() {
     Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = Maps.newHashMap();
-    controlTopicOffsets()
-        .forEach(
-            (k, v) ->
-                offsetsToCommit.put(new TopicPartition(controlTopic, k), new 
OffsetAndMetadata(v)));
-    consumer.commitSync(offsetsToCommit);
+    controlTopicOffsets.forEach(
+        (partition, offsetToCommit) -> {
+          TopicPartition tp = new TopicPartition(controlTopic, partition);
+          Long lastCommittedOffset = committedOffsets.get(tp.partition());

Review Comment:
   Nice catch, since tp only used in the conditional we can defer it's creation 
to later.



##########
kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/channel/Channel.java:
##########
@@ -142,13 +143,35 @@ protected Map<Integer, Long> controlTopicOffsets() {
     return controlTopicOffsets;
   }
 
+  /**
+   * Commit consumer offsets. Only commits offsets if it has not committed 
offsets before or the
+   * value is greater than the cached offset.
+   *
+   * <p>Note: there is a risk that two parallel coordinators may overwrite 
each other's offsets, to
+   * be fixed in a follow-up PR.
+   */
   protected void commitConsumerOffsets() {
     Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = Maps.newHashMap();
-    controlTopicOffsets()
-        .forEach(
-            (k, v) ->
-                offsetsToCommit.put(new TopicPartition(controlTopic, k), new 
OffsetAndMetadata(v)));
-    consumer.commitSync(offsetsToCommit);
+    controlTopicOffsets.forEach(
+        (partition, offsetToCommit) -> {
+          TopicPartition tp = new TopicPartition(controlTopic, partition);
+          Long lastCommittedOffset = committedOffsets.get(tp.partition());
+          if (lastCommittedOffset == null || offsetToCommit > 
lastCommittedOffset) {
+            offsetsToCommit.put(tp, new OffsetAndMetadata(offsetToCommit));
+          }
+        });
+    if (!offsetsToCommit.isEmpty()) {
+      LOG.info("Coordinator committing offsets: {}", offsetsToCommit);

Review Comment:
   Thanks, missed this the log level on this one! And updated the message



##########
kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/channel/TestCoordinator.java:
##########
@@ -316,6 +320,171 @@ private void triggerCommitCycle(Coordinator coordinator) {
     coordinator.process();
   }
 
+  @Test
+  public void testCommitConsumerOffsetsDoesNotRewind() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+
+    long healthyWatermark = 100L;
+    coordinator.controlTopicOffsets().put(0, healthyWatermark);
+    coordinator.commitConsumerOffsets();
+
+    coordinator.controlTopicOffsets().put(0, 5L);
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as("commitConsumerOffsets should not rewind the shared -coord 
consumer group offsets")
+        .isEqualTo(healthyWatermark);
+  }
+
+  @Test
+  public void testCommitConsumerDuplicateDoesNotCommit() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+
+    long healthyWatermark = 100L;
+    coordinator.controlTopicOffsets().put(0, healthyWatermark);
+    coordinator.commitConsumerOffsets();
+
+    long nextWatermark = healthyWatermark + 5;
+    consumer.commitSync(ImmutableMap.of(ctl, new 
OffsetAndMetadata(nextWatermark)));
+
+    coordinator.controlTopicOffsets().put(0, 100L);
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as(
+            "commitConsumerOffsets should not rewind offsets when consumer 
group was updated by another coordinator")
+        .isEqualTo(nextWatermark);
+  }
+
+  @Test
+  public void testCommitNewConsumerAdvances() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    long newWatermark = 5L;
+
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+    coordinator.controlTopicOffsets().put(0, newWatermark);
+
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as("commitConsumerOffsets should advance offsets on its first commit")
+        .isEqualTo(newWatermark);
+  }
+
+  @Test
+  public void testCommitConsumerAdvances() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    long healthyWatermark = 100L;
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+
+    coordinator.controlTopicOffsets().put(0, healthyWatermark);
+    coordinator.commitConsumerOffsets();
+
+    long watermarkToCommit = 105L;
+    coordinator.controlTopicOffsets().put(0, watermarkToCommit);
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as("commitConsumerOffsets should advance offsets when its value is 
greater")
+        .isEqualTo(watermarkToCommit);
+  }
+
+  @Test
+  public void testCommitConsumerMixedPartitionsRewindOrAdvance() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    long healthyWatermark0 = 100L;
+    long healthWatermark1 = 200L;
+    TopicPartition ctl0 = new TopicPartition(CTL_TOPIC_NAME, 0);
+    TopicPartition ctl1 = new TopicPartition(CTL_TOPIC_NAME, 1);
+
+    consumer.rebalance(ImmutableList.of(ctl0, ctl1));
+    consumer.updateBeginningOffsets(ImmutableMap.of(ctl0, 0L, ctl1, 0L));
+
+    coordinator.controlTopicOffsets().put(0, healthyWatermark0);
+    coordinator.controlTopicOffsets().put(1, healthWatermark1);
+    coordinator.commitConsumerOffsets();
+
+    long watermarkToCommit = 105L;
+    long watermarkToSkip = 195L;
+    coordinator.controlTopicOffsets().put(0, watermarkToCommit);
+    coordinator.controlTopicOffsets().put(1, watermarkToSkip);
+    coordinator.commitConsumerOffsets();
+
+    Map<TopicPartition, OffsetAndMetadata> committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl0, ctl1));
+
+    long committed0 =
+        committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.get(ctl0).offset();

Review Comment:
   Good point, updated as well.



##########
kafka-connect/kafka-connect/src/test/java/org/apache/iceberg/connect/channel/TestCoordinator.java:
##########
@@ -316,6 +320,171 @@ private void triggerCommitCycle(Coordinator coordinator) {
     coordinator.process();
   }
 
+  @Test
+  public void testCommitConsumerOffsetsDoesNotRewind() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+
+    long healthyWatermark = 100L;
+    coordinator.controlTopicOffsets().put(0, healthyWatermark);
+    coordinator.commitConsumerOffsets();
+
+    coordinator.controlTopicOffsets().put(0, 5L);
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as("commitConsumerOffsets should not rewind the shared -coord 
consumer group offsets")
+        .isEqualTo(healthyWatermark);
+  }
+
+  @Test
+  public void testCommitConsumerDuplicateDoesNotCommit() {
+    when(config.commitIntervalMs()).thenReturn(0);
+    when(config.commitTimeoutMs()).thenReturn(Integer.MAX_VALUE);
+
+    SinkTaskContext context = mock(SinkTaskContext.class);
+    Coordinator coordinator =
+        new Coordinator(catalog, config, ImmutableList.of(), clientFactory, 
context);
+    coordinator.start();
+    initConsumer();
+
+    TopicPartition ctl = new TopicPartition(CTL_TOPIC_NAME, 0);
+
+    long healthyWatermark = 100L;
+    coordinator.controlTopicOffsets().put(0, healthyWatermark);
+    coordinator.commitConsumerOffsets();
+
+    long nextWatermark = healthyWatermark + 5;
+    consumer.commitSync(ImmutableMap.of(ctl, new 
OffsetAndMetadata(nextWatermark)));
+
+    coordinator.controlTopicOffsets().put(0, 100L);
+    coordinator.commitConsumerOffsets();
+
+    OffsetAndMetadata committedOffsetAndMetadata =
+        consumer.committed(ImmutableSet.of(ctl)).get(ctl);
+    long committed = committedOffsetAndMetadata == null ? 0L : 
committedOffsetAndMetadata.offset();
+
+    assertThat(committed)
+        .as(
+            "commitConsumerOffsets should not rewind offsets when consumer 
group was updated by another coordinator")

Review Comment:
   Nice catch, updated and added the limitation test (tests reinforces 
documentation)



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