Savonitar commented on code in PR #334:
URL: 
https://github.com/apache/flink-connector-kafka/pull/334#discussion_r4426849407


##########
docs/content/docs/connectors/table/upsert-kafka.md:
##########
@@ -195,6 +195,22 @@ Connector Options
         format which means that key columns appear in the data type for both 
the key and value format.
       </td>
     </tr>
+    <tr>
+      <td><h5>scan.topic-partition-discovery.interval</h5></td>
+      <td>optional</td>
+      <td>no</td>

Review Comment:
   this cell lands in the incorrect column  if we check rendered version 
https://github.com/Efrat19/flink-connector-kafka/blob/3b1edb12b23e0af5af01dee09f909d29e7b2ce93/docs/content/docs/connectors/table/upsert-kafka.md#connector-options:
 
   
   <img width="929" height="529" alt="Image" 
src="https://github.com/user-attachments/assets/3fccf495-a02c-4aeb-a643-c137e57622ae";
 />
   
   The upsert-kafka table has five columns and there is no "Forwarded" column, 
but both new rows have six cells. 



##########
docs/content.zh/docs/connectors/table/upsert-kafka.md:
##########
@@ -183,6 +183,22 @@ of all available metadata fields.
        </ul>
        </td>
     </tr>
+    <tr>
+      <td><h5>scan.topic-partition-discovery.interval</h5></td>
+      <td>optional</td>
+      <td>no</td>

Review Comment:
   same 
https://github.com/apache/flink-connector-kafka/pull/334/changes#r4426849407



##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/UpsertKafkaDynamicTableFactory.java:
##########
@@ -166,6 +168,10 @@ public DynamicTableSource createDynamicTableSource(Context 
context) {
                 KafkaSourceOptions.PARTITION_DISCOVERY_INTERVAL_MS.key(),
                 Long.toString(partitionDiscoveryInterval.toMillis()));
 
+        properties.setProperty(

Review Comment:
   Same  
https://github.com/apache/flink-connector-kafka/pull/334/changes#r4427595622
   



##########
flink-connector-kafka/src/test/java/org/apache/flink/connector/kafka/dynamic/source/enumerator/DynamicKafkaSourceEnumeratorTest.java:
##########
@@ -848,6 +851,197 @@ public void 
testEnumeratorStateDoesNotContainStaleTopicPartitions() throws Throw
         }
     }
 
+    @Test
+    public void testTrackedTopicIdsSurviveMetadataChangeAndDetectRecreation() 
throws Throwable {
+        final String topic2 = TOPIC + "_integrity";
+
+        DynamicKafkaSourceTestHelper.createTopic(topic2, 
NUM_SPLITS_PER_CLUSTER, 1);
+        DynamicKafkaSourceTestHelper.produceToKafka(
+                topic2, NUM_SPLITS_PER_CLUSTER, NUM_RECORDS_PER_SPLIT);
+        final String clusterId = 
DynamicKafkaSourceTestHelper.getKafkaClusterId(0);
+
+        final Set<KafkaStream> initialStreams =
+                Collections.singleton(
+                        new KafkaStream(
+                                TOPIC,
+                                
DynamicKafkaSourceTestHelper.getClusterMetadataMap(
+                                        0, TOPIC, topic2)));
+        final Set<KafkaStream> updatedStreams =
+                Collections.singleton(
+                        new KafkaStream(
+                                TOPIC,
+                                
DynamicKafkaSourceTestHelper.getClusterMetadataMap(0, TOPIC)));
+
+        final DynamicKafkaSourceEnumState stateAfterMetadataChange;
+        final Map<String, String> initialTrackedIds;
+        try (MockKafkaMetadataService metadataService =
+                        new MockKafkaMetadataService(initialStreams);
+                MockSplitEnumeratorContext<DynamicKafkaSourceSplit> context =
+                        new MockSplitEnumeratorContext<>(NUM_SUBTASKS);
+                DynamicKafkaSourceEnumerator enumerator =
+                        createEnumerator(
+                                context,
+                                metadataService,
+                                
DynamicKafkaSourceEnumeratorTest::enableTopicIntegrityCheck)) {
+            enumerator.start();
+            context.runPeriodicCallable(0);
+            runAllOneTimeCallables(context);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
0);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
1);
+
+            initialTrackedIds = 
getTrackedTopicIds(enumerator.snapshotState(-1));
+            assertThat(initialTrackedIds).containsOnlyKeys(TOPIC, topic2);
+
+            metadataService.setKafkaStreams(updatedStreams);
+            context.runPeriodicCallable(0);
+
+            // the restarted enumerators have not rediscovered the topics yet, 
so the ids can only
+            // come from the state carried over the metadata change
+            assertThat(getTrackedTopicIds(enumerator.snapshotState(-1)))
+                    .containsOnly(Map.entry(TOPIC, 
initialTrackedIds.get(TOPIC)));
+
+            // the rediscovery agrees with the carried over id, so the check 
passes
+            runAllOneTimeCallables(context);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
2);
+
+            stateAfterMetadataChange = enumerator.snapshotState(-1);
+            assertThat(getTrackedTopicIds(stateAfterMetadataChange))
+                    .containsOnly(Map.entry(TOPIC, 
initialTrackedIds.get(TOPIC)));
+        }
+
+        // a stale id in the checkpoint is what a topic recreated after the 
checkpoint looks like
+        final KafkaSourceEnumState clusterState =
+                
stateAfterMetadataChange.getClusterEnumeratorStates().get(clusterId);
+        final String staleTopicId = Uuid.randomUuid().toString();
+        final DynamicKafkaSourceEnumState staleState =
+                withClusterState(
+                        stateAfterMetadataChange,
+                        clusterId,
+                        new KafkaSourceEnumState(
+                                clusterState.splits(),
+                                clusterState.initialDiscoveryFinished(),
+                                Collections.singletonMap(TOPIC, 
staleTopicId)));
+
+        // topic2 is back in the stream, so the restored enumerators are 
restarted on the first
+        // metadata refresh before they run their first discovery
+        try (MockKafkaMetadataService metadataService =
+                        new MockKafkaMetadataService(initialStreams);
+                MockSplitEnumeratorContext<DynamicKafkaSourceSplit> context =
+                        new MockSplitEnumeratorContext<>(NUM_SUBTASKS);
+                DynamicKafkaSourceEnumerator enumerator =
+                        createEnumerator(
+                                context,
+                                metadataService,
+                                
DynamicKafkaSourceEnumeratorTest::enableTopicIntegrityCheck,
+                                staleState)) {
+            enumerator.start();
+            context.runPeriodicCallable(0);
+
+            assertThat(getTrackedTopicIds(enumerator.snapshotState(-1)))
+                    .containsOnly(Map.entry(TOPIC, staleTopicId));
+            assertThatThrownBy(() -> runAllOneTimeCallables(context))
+                    .hasRootCauseInstanceOf(TopicIntegrityException.class)
+                    .hasRootCauseMessage("Topic " + TOPIC + " was recreated");
+        }
+    }
+
+    @Test
+    public void testRestoreKeepsTrackedTopicIdsOfTopicsWithUnassignedSplits() 
throws Throwable {
+        final String topic2 = TOPIC + "_unassigned";
+        DynamicKafkaSourceTestHelper.createTopic(topic2, 
NUM_SPLITS_PER_CLUSTER, 1);
+        DynamicKafkaSourceTestHelper.produceToKafka(
+                topic2, NUM_SPLITS_PER_CLUSTER, NUM_RECORDS_PER_SPLIT);
+        final String clusterId = 
DynamicKafkaSourceTestHelper.getKafkaClusterId(0);
+        final KafkaStream stream =
+                new KafkaStream(
+                        TOPIC,
+                        DynamicKafkaSourceTestHelper.getClusterMetadataMap(0, 
TOPIC, topic2));
+
+        final DynamicKafkaSourceEnumState checkpointState =
+                getCheckpointStateWithTopicIntegrityCheck(stream);
+        final KafkaSourceEnumState clusterState =
+                checkpointState.getClusterEnumeratorStates().get(clusterId);
+        final Map<String, String> trackedIds = 
clusterState.trackedTopicIdsByName();
+        assertThat(trackedIds).containsOnlyKeys(TOPIC, topic2);
+
+        // a checkpoint taken before the splits of topic2 were handed to a 
reader
+        final Set<SplitAndAssignmentStatus> splits =
+                clusterState.splits().stream()
+                        .map(
+                                splitStatus ->
+                                        
topic2.equals(splitStatus.split().getTopic())
+                                                ? new SplitAndAssignmentStatus(
+                                                        splitStatus.split(),
+                                                        
AssignmentStatus.UNASSIGNED)
+                                                : splitStatus)
+                        .collect(Collectors.toSet());
+        final DynamicKafkaSourceEnumState stateWithUnassignedTopic =
+                withClusterState(
+                        checkpointState,
+                        clusterId,
+                        new KafkaSourceEnumState(
+                                splits, 
clusterState.initialDiscoveryFinished(), trackedIds));
+
+        try (MockKafkaMetadataService metadataService =
+                        new 
MockKafkaMetadataService(Collections.singleton(stream));
+                MockSplitEnumeratorContext<DynamicKafkaSourceSplit> context =
+                        new MockSplitEnumeratorContext<>(NUM_SUBTASKS);
+                DynamicKafkaSourceEnumerator enumerator =
+                        createEnumerator(
+                                context,
+                                metadataService,
+                                
DynamicKafkaSourceEnumeratorTest::enableTopicIntegrityCheck,
+                                stateWithUnassignedTopic)) {
+            enumerator.start();
+            // the restored sub-enumerator discovers before the first metadata 
refresh
+            runAllOneTimeCallables(context);
+
+            assertThat(getTrackedTopicIds(enumerator.snapshotState(-1)))
+                    .containsExactlyInAnyOrderEntriesOf(trackedIds);
+        }
+    }
+
+    private static void enableTopicIntegrityCheck(Properties properties) {
+        properties.setProperty(
+                
DynamicKafkaSourceOptions.STREAM_METADATA_DISCOVERY_INTERVAL_MS.key(), "1");
+        
properties.setProperty(KafkaSourceOptions.TOPIC_INTEGRITY_CHECK_ENABLED.key(), 
"true");
+    }
+
+    private DynamicKafkaSourceEnumState 
getCheckpointStateWithTopicIntegrityCheck(
+            KafkaStream kafkaStream) throws Throwable {
+        try (MockSplitEnumeratorContext<DynamicKafkaSourceSplit> context =
+                        new MockSplitEnumeratorContext<>(NUM_SUBTASKS);
+                DynamicKafkaSourceEnumerator enumerator =
+                        createEnumerator(
+                                context,
+                                new 
MockKafkaMetadataService(Collections.singleton(kafkaStream)),
+                                
DynamicKafkaSourceEnumeratorTest::enableTopicIntegrityCheck)) {
+            enumerator.start();
+            context.runPeriodicCallable(0);
+            runAllOneTimeCallables(context);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
0);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
1);
+            mockRegisterReaderAndSendReaderStartupEvent(context, enumerator, 
2);
+            return enumerator.snapshotState(-1);
+        }
+    }
+
+    private static DynamicKafkaSourceEnumState withClusterState(
+            DynamicKafkaSourceEnumState state,
+            String clusterId,
+            KafkaSourceEnumState clusterState) {
+        final Map<String, KafkaSourceEnumState> clusterStates =
+                new HashMap<>(state.getClusterEnumeratorStates());
+        clusterStates.put(clusterId, clusterState);
+        return new DynamicKafkaSourceEnumState(state.getKafkaStreams(), 
clusterStates);
+    }
+
+    private static Map<String, String> 
getTrackedTopicIds(DynamicKafkaSourceEnumState state) {

Review Comment:
   If this helper is reused with integrity checking enabled and the default 
two-cluster fixture, the same topic name occurs twice and toMap throws 
"Duplicate key"?



##########
flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/dynamic/source/enumerator/StoppableKafkaEnumContextProxy.java:
##########
@@ -276,6 +276,20 @@ protected <T> Callable<T> 
wrapCallAsyncCallable(Callable<T> callable) {
         };
     }
 
+    /**
+     * The topic integrity check reports a missing or recreated topic as a 
{@link
+     * TopicIntegrityException} without the broker exception as cause, so it 
has to be matched on
+     * its own to be swallowed for a cluster that is no longer active.
+     */
+    private static Optional<? extends Throwable> 
findKafkaRelatedThrowable(Throwable t) {

Review Comment:
   Does it make sense to use `ExceptionUtils.findThrowable(Throwable, 
Predicate<Throwable>)` ? it can do this, so the helper and the wildcard 
`Optional` can go:
   
   ```java
   Optional<Throwable> throwable =
           ExceptionUtils.findThrowable(
                   e, t -> t instanceof KafkaException || t instanceof 
TopicIntegrityException);
   ```
   
   WDYT?



##########
docs/content/docs/connectors/datastream/dynamic-kafka.md:
##########
@@ -317,6 +317,14 @@ There are configuration options in 
DynamicKafkaSourceOptions that can be configu
 In addition to this list, see the [regular Kafka connector]({{< ref 
"docs/connectors/datastream/kafka" >}}#additional-properties) for
 a list of applicable properties.
 
+### Topic Integrity Check
+Setting the property `scan.topic-integrity-check.enabled` to `true` enables the

Review Comment:
   Could we show how to set it, like the KafkaSource page does?



##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/DynamicKafkaTableFactory.java:
##########
@@ -161,6 +163,10 @@ public DynamicTableSource createDynamicTableSource(Context 
context) {
                 KafkaSourceOptions.PARTITION_DISCOVERY_INTERVAL_MS.key(),
                 Long.toString(partitionDiscoveryInterval.toMillis()));
 
+        properties.setProperty(

Review Comment:
   Same  
https://github.com/apache/flink-connector-kafka/pull/334/changes#r4427595622
   



##########
flink-connector-kafka/src/main/java/org/apache/flink/streaming/connectors/kafka/table/KafkaDynamicTableFactory.java:
##########
@@ -222,6 +225,10 @@ public DynamicTableSource createDynamicTableSource(Context 
context) {
 
         final String keyPrefix = 
tableOptions.getOptional(KEY_FIELDS_PREFIX).orElse(null);
 
+        properties.setProperty(

Review Comment:
   When the top-level option is omitted, 
`tableOptions.get(SCAN_TOPIC_INTEGRITY_CHECK_ENABLED)` returns its default 
false. 
   This overwrites an existing 'properties.scan.topic-integrity-check.enabled' 
= 'true', and silently disabling checks that were enabled before this change. 
   
   Could we apply this override only when the top-level option is explicitly 
configured? Explicit false should still take precedence, while an absent option 
should preserve the existing setting.  



##########
docs/content/docs/connectors/datastream/dynamic-kafka.md:
##########
@@ -317,6 +317,14 @@ There are configuration options in 
DynamicKafkaSourceOptions that can be configu
 In addition to this list, see the [regular Kafka connector]({{< ref 
"docs/connectors/datastream/kafka" >}}#additional-properties) for
 a list of applicable properties.
 
+### Topic Integrity Check
+Setting the property `scan.topic-integrity-check.enabled` to `true` enables the
+[topic integrity check]({{< ref "docs/connectors/datastream/kafka" 
>}}#topic-integrity-check) for the topics
+subscribed on every Kafka cluster of the stream: the source fails with a 
`TopicIntegrityException` if a

Review Comment:
   Do we want an `enableTopicIntegrityCheck()` on `DynamicKafkaSourceBuilder`, 
like `KafkaSourceBuilder` has?



##########
flink-connector-kafka/src/test/java/org/apache/flink/connector/kafka/dynamic/source/enumerator/DynamicKafkaSourceEnumeratorTest.java:
##########
@@ -35,8 +35,10 @@
 import org.apache.flink.connector.kafka.source.KafkaSourceOptions;
 import org.apache.flink.connector.kafka.source.enumerator.AssignmentStatus;
 import org.apache.flink.connector.kafka.source.enumerator.KafkaSourceEnumState;
+import 
org.apache.flink.connector.kafka.source.enumerator.SplitAndAssignmentStatus;

Review Comment:
   Now DynamicKafkaSourceEnumeratorTest has 3117 lines and exceeds the 
Checkstyle limit of 3100.
   
https://github.com/Efrat19/flink-connector-kafka/actions/runs/40704407963/job/122872517230#step:17:134
   Could we move the topic-integrity tests and their helpers into a separate 
test class?



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

Reply via email to