This is an automated email from the ASF dual-hosted git repository. 1996fanrui pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/flink-connector-kafka.git
commit 9cc364d80db5c3906dd4afe2b8817b90c548fb36 Author: Efrat Levitan <[email protected]> AuthorDate: Sun Jul 12 20:22:56 2026 +0300 [FLINK-40128][connector] Add an opt-in based topic integrity config option Currently, when a user decides to delete/recreate a kafka source topic, the flink job silently adapts. This commit adds an opt-in config allowing to verify the integrity of a kafka source topic and fail it if it is missing in broker invenroty (deleted) or presented with a different topic id (=recreated) --- .../content.zh/docs/connectors/datastream/kafka.md | 32 +++++++++++++++++++ docs/content/docs/connectors/datastream/kafka.md | 37 ++++++++++++++++++++++ .../connector/kafka/source/KafkaSourceBuilder.java | 26 +++++++++++++++ .../connector/kafka/source/KafkaSourceOptions.java | 7 ++++ 4 files changed, 102 insertions(+) diff --git a/docs/content.zh/docs/connectors/datastream/kafka.md b/docs/content.zh/docs/connectors/datastream/kafka.md index 26c1f2ac..52b2691e 100644 --- a/docs/content.zh/docs/connectors/datastream/kafka.md +++ b/docs/content.zh/docs/connectors/datastream/kafka.md @@ -220,6 +220,8 @@ Kafka Source 支持流式和批式两种运行模式。默认情况下,KafkaSo - ```commit.offsets.on.checkpoint``` 指定是否在进行 checkpoint 时将消费位点提交至 Kafka broker - ```poll.timeout.ms``` 指定 Kafka Consumer 单次 poll 等待数据的最长时间(毫秒),默认为 10 秒。 减小该值可以让空闲的 Source 更快地响应分片变更,但 poll 会更频繁 +- ```scan.topic-integrity-check.enabled``` 指定是否在运行时校验订阅 Topic 的 id,并在 Topic 缺失或被重建时使作业失败。 + 默认关闭。详见下面的<a href="#topic-integrity-check">Topic 完整性检查</a>一节 Kafka consumer 的配置可以参考 [Apache Kafka 文档](http://kafka.apache.org/documentation/#consumerconfigs)。 @@ -251,6 +253,36 @@ KafkaSource.builder() \ 分区检查间隔默认为5分钟。需要显式地设置分区检查间隔为非正数才能关闭此功能。 {{< /hint >}} +### Topic 完整性检查 +如果某个订阅的 Topic 被删除后又以相同的名称重建,Kafka Source 默认会像什么都没发生一样,继续从新的 Topic 静默地消费数据, +而这通常并非用户所期望的行为。Topic 完整性检查通过跟踪每个订阅 Topic 的 topic id 来防止这种情况:一旦检测到 topic id +不匹配(Topic 被重建)或 Topic 缺失,就会使作业失败,而不是静默地继续从不同的底层 Topic 中读取数据。 + +要启用该功能,可以在 ```KafkaSourceBuilder``` 上调用 ```enableTopicIntegrityCheck()```,或者将属性 +```scan.topic-integrity-check.enabled``` 设置为 ```true```: + +{{< tabs "KafkaSource#TopicIntegrityCheck" >}} +{{< tab "Java" >}} +```java +KafkaSource.builder() + .enableTopicIntegrityCheck(); +``` +{{< /tab >}} +{{< /tabs >}} + +{{< hint info >}} +Topic 完整性检查仅在通过 ```setTopics(String...)```、```setTopicPattern(Pattern)``` 或 +```setPartitions(Set)``` 订阅 Topic 时受支持。如果将此选项与未实现 ```TopicMetadataSettable``` 接口的 +自定义 ```KafkaSubscriber```(通过 ```setKafkaSubscriber``` 设置)一起启用,构建器会在构建时抛出异常。 +{{< /hint >}} + +该检查会在 Source 每次从 Kafka 拉取 Topic / Partition 元数据时执行,即启动时执行一次;如果启用了 +<a href="#dynamic-partition-discovery">动态分区检查</a>,之后每个检查周期还会再执行一次。要在作业运行期间 +周期性地执行该检查,请确保 ```partition.discovery.interval.ms``` 被设置为正值;否则该检查只会在 +Source 启动或恢复时执行一次。 + +当某个 Topic 被检测为缺失或被重建时,Source 会抛出 ```TopicIntegrityException``` 并使作业失败。 + ### 事件时间和水印 默认情况下,Kafka Source 使用 Kafka 消息中的时间戳作为事件时间。您可以定义自己的水印策略(Watermark Strategy) 以从消息中提取事件时间,并向下游发送水印: diff --git a/docs/content/docs/connectors/datastream/kafka.md b/docs/content/docs/connectors/datastream/kafka.md index 20290297..d134a17b 100644 --- a/docs/content/docs/connectors/datastream/kafka.md +++ b/docs/content/docs/connectors/datastream/kafka.md @@ -231,6 +231,9 @@ metric group - ```poll.timeout.ms``` defines the maximum time in milliseconds the Kafka consumer blocks in a single poll while waiting for records, 10 seconds by default. Lowering it makes an idle source react faster to split changes, at the cost of polling more often +- ```scan.topic-integrity-check.enabled``` specifies whether to verify the id of the subscribed + topics during runtime and fail the job if a topic is missing or was recreated. Disabled by + default. See <a href="#topic-integrity-check">Topic Integrity Check</a> below for more details. For configurations of KafkaConsumer, you can refer to <a href="http://kafka.apache.org/documentation/#consumerconfigs">Apache Kafka documentation</a> @@ -269,6 +272,40 @@ KafkaSource.builder() \ The partition discovery interval is 5 minutes by default. To **disable** this feature, you need to explicitly set the partition discovery interval to a non-positive value. {{< /hint >}} +### Topic Integrity Check +If a subscribed topic is deleted and later recreated with the same name, the Kafka source would by +default silently keep consuming from the new topic as if nothing happened, which is often not what +users want. Topic integrity check protects against this scenario by tracking the topic id of every +subscribed topic and failing the job as soon as a mismatch (topic recreated) or a missing topic is +detected, instead of silently continuing to read from a different underlying topic. + +To enable this feature, either call ```enableTopicIntegrityCheck()``` on the ```KafkaSourceBuilder```, +or set the property ```scan.topic-integrity-check.enabled``` to ```true```: + +{{< tabs "KafkaSource#TopicIntegrityCheck" >}} +{{< tab "Java" >}} +```java +KafkaSource.builder() + .enableTopicIntegrityCheck(); +``` +{{< /tab >}} +{{< /tabs >}} + +{{< hint info >}} +Topic integrity check is only supported when the source subscribes to topics using +```setTopics(String...)```, ```setTopicPattern(Pattern)``` or ```setPartitions(Set)```. Enabling +this option together with a custom ```KafkaSubscriber``` (via ```setKafkaSubscriber```) that does +not implement ```TopicMetadataSettable``` will cause the builder to throw an exception at build time. +{{< /hint >}} + +The check is performed every time the source fetches topic/partition metadata from Kafka, i.e. once +at startup and, if <a href="#dynamic-partition-discovery">partition discovery</a> is enabled, once +per discovery interval afterwards. To have the check performed periodically while the job is +running, make sure ```partition.discovery.interval.ms``` is set to a positive value; otherwise the +topics are only checked once during source startup or recovery. + +When a topic is found missing or recreated, the source fails with a ```TopicIntegrityException```. + ### Event Time and Watermarks By default, the record will use the timestamp embedded in Kafka ```ConsumerRecord``` as the event time. You can define your own ```WatermarkStrategy``` for extract event time from the record itself, diff --git a/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceBuilder.java b/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceBuilder.java index 4167f385..b88d6711 100644 --- a/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceBuilder.java +++ b/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceBuilder.java @@ -24,6 +24,7 @@ import org.apache.flink.api.connector.source.Boundedness; import org.apache.flink.connector.kafka.source.enumerator.initializer.NoStoppingOffsetsInitializer; import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer; import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializerValidator; +import org.apache.flink.connector.kafka.source.enumerator.metadata.TopicMetadataSettable; import org.apache.flink.connector.kafka.source.enumerator.subscriber.KafkaSubscriber; import org.apache.flink.connector.kafka.source.reader.deserializer.KafkaRecordDeserializationSchema; import org.apache.flink.util.function.SerializableSupplier; @@ -375,6 +376,19 @@ public class KafkaSourceBuilder<OUT> { return this; } + /** + * Whether perform a check for the integrity of source topics, and fail the job if the topic was + * deleted or recreated. For the check to be performed periodically during source runtime, + * partition discovery must be enabled (KafkaSourceOptions.PARTITION_DISCOVERY_INTERVAL_MS set + * to a positive number) + * + * @return {@link KafkaSourceBuilder} + */ + public KafkaSourceBuilder<OUT> enableTopicIntegrityCheck() { + this.setProperty(KafkaSourceOptions.TOPIC_INTEGRITY_CHECK_ENABLED.key(), "true"); + return this; + } + /** * Set an arbitrary property for the KafkaSource and KafkaConsumer. The valid keys can be found * in {@link ConsumerConfig} and {@link KafkaSourceOptions}. @@ -561,6 +575,12 @@ public class KafkaSourceBuilder<OUT> { if (props.containsKey(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG)) { checkDeserializer(props.getProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG)); } + if (topicIntegrityCheckEnabled()) { + checkState( + subscriber instanceof TopicMetadataSettable, + "Topic integrity check is not supported for non TopicMetadataSettable subscriber %s", + subscriber.getClass().getName()); + } } private void checkDeserializer(String deserializer) { @@ -610,4 +630,10 @@ public class KafkaSourceBuilder<OUT> { KafkaSourceOptions.COMMIT_OFFSETS_ON_CHECKPOINT.key())); return autoCommit || commitOnCheckpoint; } + + private boolean topicIntegrityCheckEnabled() { + return props.containsKey(KafkaSourceOptions.TOPIC_INTEGRITY_CHECK_ENABLED.key()) + && Boolean.parseBoolean( + props.getProperty(KafkaSourceOptions.TOPIC_INTEGRITY_CHECK_ENABLED.key())); + } } diff --git a/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceOptions.java b/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceOptions.java index 5928d8a2..188c8f9d 100644 --- a/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceOptions.java +++ b/flink-connector-kafka/src/main/java/org/apache/flink/connector/kafka/source/KafkaSourceOptions.java @@ -70,6 +70,13 @@ public class KafkaSourceOptions { + "of polling more often. 0 polls without blocking. Must not be " + "negative."); + public static final ConfigOption<Boolean> TOPIC_INTEGRITY_CHECK_ENABLED = + ConfigOptions.key("scan.topic-integrity-check.enabled") + .booleanType() + .defaultValue(false) + .withDescription( + "Whether to verify topic id during runtime and fail if the topic is missing or recreated"); + @SuppressWarnings("unchecked") public static <T> T getOption( Properties props, ConfigOption<?> configOption, Function<String, T> parser) {
