-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-4936: Add dynamic routing in Streams #5018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
a287ef1
4205260
5d689a9
1cec567
b5bd7b5
a6a6ed9
3f7fdf2
74ff612
2cf65e0
2cda250
5cb8802
5979e37
f3c18c6
0519aa5
0c503b9
86fde2f
dc8b9fe
2e7eeb4
55ae4e7
d1ce394
ace38a1
7f2b7af
35d472e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import org.apache.kafka.streams.processor.StateStore; | ||
| import org.apache.kafka.streams.processor.StreamPartitioner; | ||
| import org.apache.kafka.streams.processor.TimestampExtractor; | ||
| import org.apache.kafka.streams.processor.TopicNameExtractor; | ||
| import org.apache.kafka.streams.processor.internals.InternalTopologyBuilder; | ||
| import org.apache.kafka.streams.processor.internals.ProcessorNode; | ||
| import org.apache.kafka.streams.processor.internals.ProcessorTopology; | ||
|
|
@@ -516,6 +517,126 @@ public synchronized <K, V> Topology addSink(final String name, | |
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Add a new sink that forwards records from upstream parent processor and/or source nodes to Kafka topics dynamically. | ||
| * The topics that it may ever send to should be pre-created. | ||
| * The sink will use the {@link StreamsConfig#DEFAULT_KEY_SERDE_CLASS_CONFIG default key serializer} and | ||
| * {@link StreamsConfig#DEFAULT_VALUE_SERDE_CLASS_CONFIG default value serializer} specified in the | ||
| * {@link StreamsConfig stream configuration}. | ||
| * | ||
| * @param name the unique name of the sink | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to which this sink should write for reach record | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
| * @param parentNames the name of one or more source or processor nodes whose output records this sink should consume | ||
| * and dynamically write to topics | ||
| * @return itself | ||
| * @throws TopologyException if parent processor is not added yet, or if this processor's name is not unique | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we throw for all three cases.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. If
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense, will add it back. |
||
| * @see #addSink(String, String, StreamPartitioner, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, StreamPartitioner, String...) | ||
| */ | ||
| public synchronized <K, V> Topology addSink(final String name, | ||
| final TopicNameExtractor<K, V> topicExtractor, | ||
| final String... parentNames) { | ||
| internalTopologyBuilder.addSink(name, topicExtractor, null, null, null, parentNames); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Add a new sink that forwards records from upstream parent processor and/or source nodes to Kafka topics dynamically, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above. |
||
| * using the supplied partitioner. | ||
| * The topics that it may ever send to should be pre-created. | ||
| * The sink will use the {@link StreamsConfig#DEFAULT_KEY_SERDE_CLASS_CONFIG default key serializer} and | ||
| * {@link StreamsConfig#DEFAULT_VALUE_SERDE_CLASS_CONFIG default value serializer} specified in the | ||
| * {@link StreamsConfig stream configuration}. | ||
| * <p> | ||
| * The sink will also use the specified {@link StreamPartitioner} to determine how records are distributed among | ||
| * the named Kafka topic's partitions. | ||
| * Such control is often useful with topologies that use {@link #addStateStore(StoreBuilder, String...) state | ||
| * stores} in its processors. | ||
| * In most other cases, however, a partitioner needs not be specified and Kafka will automatically distribute | ||
| * records among partitions using Kafka's default partitioning logic. | ||
| * | ||
| * @param name the unique name of the sink | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to which this sink should write for reach record | ||
| * @param partitioner the function that should be used to determine the partition for each record processed by the sink | ||
| * @param parentNames the name of one or more source or processor nodes whose output records this sink should consume | ||
| * and dynamically write to topics | ||
| * @return itself | ||
| * @throws TopologyException if parent processor is not added yet, or if this processor's name is not unique | ||
| * @see #addSink(String, String, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, StreamPartitioner, String...) | ||
| */ | ||
| public synchronized <K, V> Topology addSink(final String name, | ||
| final TopicNameExtractor<K, V> topicExtractor, | ||
| final StreamPartitioner<? super K, ? super V> partitioner, | ||
| final String... parentNames) { | ||
| internalTopologyBuilder.addSink(name, topicExtractor, null, null, partitioner, parentNames); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Add a new sink that forwards records from upstream parent processor and/or source nodes to Kafka topics dynamically. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above |
||
| * The topics that it may ever send to should be pre-created. | ||
| * The sink will use the specified key and value serializers. | ||
| * | ||
| * @param name the unique name of the sink | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to which this sink should write for reach record | ||
| * @param keySerializer the {@link Serializer key serializer} used when consuming records; may be null if the sink | ||
| * should use the {@link StreamsConfig#DEFAULT_KEY_SERDE_CLASS_CONFIG default key serializer} specified in the | ||
| * {@link StreamsConfig stream configuration} | ||
| * @param valueSerializer the {@link Serializer value serializer} used when consuming records; may be null if the sink | ||
| * should use the {@link StreamsConfig#DEFAULT_VALUE_SERDE_CLASS_CONFIG default value serializer} specified in the | ||
| * {@link StreamsConfig stream configuration} | ||
| * @param parentNames the name of one or more source or processor nodes whose output records this sink should consume | ||
| * and dynamically write to topics | ||
| * @return itself | ||
| * @throws TopologyException if parent processor is not added yet, or if this processor's name is not unique | ||
| * @see #addSink(String, String, String...) | ||
| * @see #addSink(String, String, StreamPartitioner, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, StreamPartitioner, String...) | ||
| */ | ||
| public synchronized <K, V> Topology addSink(final String name, | ||
| final TopicNameExtractor<K, V> topicExtractor, | ||
| final Serializer<K> keySerializer, | ||
| final Serializer<V> valueSerializer, | ||
| final String... parentNames) { | ||
| internalTopologyBuilder.addSink(name, topicExtractor, keySerializer, valueSerializer, null, parentNames); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Add a new sink that forwards records from upstream parent processor and/or source nodes to Kafka topics dynamically. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
| * The topics that it may ever send to should be pre-created. | ||
| * The sink will use the specified key and value serializers, and the supplied partitioner. | ||
| * | ||
| * @param name the unique name of the sink | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to which this sink should write for reach record | ||
| * @param keySerializer the {@link Serializer key serializer} used when consuming records; may be null if the sink | ||
| * should use the {@link StreamsConfig#DEFAULT_KEY_SERDE_CLASS_CONFIG default key serializer} specified in the | ||
| * {@link StreamsConfig stream configuration} | ||
| * @param valueSerializer the {@link Serializer value serializer} used when consuming records; may be null if the sink | ||
| * should use the {@link StreamsConfig#DEFAULT_VALUE_SERDE_CLASS_CONFIG default value serializer} specified in the | ||
| * {@link StreamsConfig stream configuration} | ||
| * @param partitioner the function that should be used to determine the partition for each record processed by the sink | ||
| * @param parentNames the name of one or more source or processor nodes whose output records this sink should consume | ||
| * and dynamically write to topics | ||
| * @return itself | ||
| * @throws TopologyException if parent processor is not added yet, or if this processor's name is not unique | ||
| * @see #addSink(String, String, String...) | ||
| * @see #addSink(String, String, StreamPartitioner, String...) | ||
| * @see #addSink(String, String, Serializer, Serializer, String...) | ||
| */ | ||
| public synchronized <K, V> Topology addSink(final String name, | ||
| final TopicNameExtractor<K, V> topicExtractor, | ||
| final Serializer<K> keySerializer, | ||
| final Serializer<V> valueSerializer, | ||
| final StreamPartitioner<? super K, ? super V> partitioner, | ||
| final String... parentNames) { | ||
| internalTopologyBuilder.addSink(name, topicExtractor, keySerializer, valueSerializer, partitioner, parentNames); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Add a new processor node that receives and processes records output by one or more parent source or processor | ||
| * node. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| import org.apache.kafka.streams.processor.ProcessorContext; | ||
| import org.apache.kafka.streams.processor.ProcessorSupplier; | ||
| import org.apache.kafka.streams.processor.StreamPartitioner; | ||
| import org.apache.kafka.streams.processor.TopicNameExtractor; | ||
|
|
||
| /** | ||
| * {@code KStream} is an abstraction of a <i>record stream</i> of {@link KeyValue} pairs, i.e., each record is an | ||
|
|
@@ -461,12 +462,31 @@ KStream<K, V> through(final String topic, | |
| * The specified topic should be manually created before it is used (i.e., before the Kafka Streams application is | ||
| * started). | ||
| * | ||
| * @param produced the options to use when producing to the topic | ||
| * @param topic the topic name | ||
| * @param produced the options to use when producing to the topic | ||
| */ | ||
| void to(final String topic, | ||
| final Produced<K, V> produced); | ||
|
|
||
| /** | ||
| * Dynamically materialize this stream to topics using default serializers specified in the config and producer's | ||
| * {@link DefaultPartitioner}. | ||
| * The topic names for each record to send to is dynamically determined based on the {@link TopicNameExtractor}. | ||
| * | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to write to for reach record | ||
| */ | ||
| void to(final TopicNameExtractor<K, V> topicExtractor); | ||
|
|
||
| /** | ||
| * Dynamically materialize this stream to topics using default serializers specified in the config. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one doesn't use the default serializers, but instead uses |
||
| * The topic names for each record to send to is dynamically determined based on the {@link TopicNameExtractor}. | ||
| * | ||
| * @param topicExtractor the extractor to determine the name of the Kafka topic to write to for reach record | ||
| * @param produced the options to use when producing to the topic | ||
| */ | ||
| void to(final TopicNameExtractor<K, V> topicExtractor, | ||
| final Produced<K, V> produced); | ||
|
|
||
| /** | ||
| * Transform each record of the input stream into zero or more records in the output stream (both key and value type | ||
| * can be altered arbitrarily). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| import org.apache.kafka.streams.processor.FailOnInvalidTimestamp; | ||
| import org.apache.kafka.streams.processor.ProcessorSupplier; | ||
| import org.apache.kafka.streams.processor.StreamPartitioner; | ||
| import org.apache.kafka.streams.processor.TopicNameExtractor; | ||
| import org.apache.kafka.streams.state.StoreBuilder; | ||
| import org.apache.kafka.streams.state.Stores; | ||
| import org.apache.kafka.streams.state.WindowStore; | ||
|
|
@@ -304,13 +305,24 @@ public void to(final String topic) { | |
| to(topic, Produced.<K, V>with(null, null, null)); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| @Override | ||
| public void to(final String topic, final Produced<K, V> produced) { | ||
| Objects.requireNonNull(topic, "topic can't be null"); | ||
| Objects.requireNonNull(topic, "topic chooser can't be null"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is no topic chooser here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @guozhangwang missed this one (just a nit though)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, missed this one.. updated. |
||
| Objects.requireNonNull(produced, "Produced can't be null"); | ||
| to(topic, new ProducedInternal<>(produced)); | ||
| } | ||
|
|
||
| @Override | ||
| public void to(final TopicNameExtractor<K, V> topicExtractor) { | ||
| Objects.requireNonNull(topicExtractor, "topic extractor can't be null"); | ||
| to(topicExtractor, Produced.<K, V>with(null, null, null)); | ||
| } | ||
|
|
||
| @Override | ||
| public void to(final TopicNameExtractor<K, V> topicExtractor, final Produced<K, V> produced) { | ||
| Objects.requireNonNull(topicExtractor, "topic extractor can't be null"); | ||
| Objects.requireNonNull(produced, "Produced can't be null"); | ||
| to(topicExtractor, new ProducedInternal<>(produced)); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
|
|
@@ -328,6 +340,21 @@ private void to(final String topic, final ProducedInternal<K, V> produced) { | |
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private void to(final TopicNameExtractor<K, V> topicExtractor, final ProducedInternal<K, V> produced) { | ||
| final String name = builder.newProcessorName(SINK_NAME); | ||
| final Serializer<K> keySerializer = produced.keySerde() == null ? null : produced.keySerde().serializer(); | ||
| final Serializer<V> valSerializer = produced.valueSerde() == null ? null : produced.valueSerde().serializer(); | ||
| final StreamPartitioner<? super K, ? super V> partitioner = produced.streamPartitioner(); | ||
|
|
||
| if (partitioner == null && keySerializer instanceof WindowedSerializer) { | ||
| final StreamPartitioner<K, V> windowedPartitioner = (StreamPartitioner<K, V>) new WindowedStreamPartitioner<Object, V>((WindowedSerializer) keySerializer); | ||
| builder.internalTopologyBuilder.addSink(name, topicExtractor, keySerializer, valSerializer, windowedPartitioner, this.name); | ||
| } else { | ||
| builder.internalTopologyBuilder.addSink(name, topicExtractor, keySerializer, valSerializer, partitioner, this.name); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public <K1, V1> KStream<K1, V1> transform(final TransformerSupplier<? super K, ? super V, KeyValue<K1, V1>> transformerSupplier, | ||
| final String... stateStoreNames) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,15 @@ public class WindowedStreamPartitioner<K, V> implements StreamPartitioner<Window | |
| this.serializer = serializer; | ||
| } | ||
|
|
||
| WindowedStreamPartitioner(final WindowedSerializer<K> serializer) { | ||
| this.topic = null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need an overload for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've thought about it before, but this is a bit tricky since the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see -- should we do a KIP for this to fix it? We can just create a JIRA for now -- maybe somebody wants to pick it up. |
||
| this.serializer = serializer; | ||
| } | ||
|
|
||
| public String topic() { | ||
| return this.topic; | ||
| } | ||
|
|
||
| /** | ||
| * WindowedStreamPartitioner determines the partition number for a record with the given windowed key and value | ||
| * and the current number of partitions. The partition number id determined by the original key of the windowed key | ||
|
|
@@ -42,10 +51,18 @@ public class WindowedStreamPartitioner<K, V> implements StreamPartitioner<Window | |
| * @param numPartitions the total number of partitions | ||
| * @return an integer between 0 and {@code numPartitions-1}, or {@code null} if the default partitioning logic should be used | ||
| */ | ||
| @Override | ||
| public Integer partition(final Windowed<K> windowedKey, final V value, final int numPartitions) { | ||
| final byte[] keyBytes = serializer.serializeBaseKey(topic, windowedKey); | ||
|
|
||
| // hash the keyBytes to choose a partition | ||
| return toPositive(Utils.murmur2(keyBytes)) % numPartitions; | ||
| } | ||
|
|
||
| public Integer partition(final String topic, final Windowed<K> windowedKey, final V value, final int numPartitions) { | ||
| final byte[] keyBytes = serializer.serializeBaseKey(topic, windowedKey); | ||
|
|
||
| // hash the keyBytes to choose a partition | ||
| return toPositive(Utils.murmur2(keyBytes)) % numPartitions; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.kafka.streams.processor; | ||
|
|
||
| import org.apache.kafka.common.annotation.InterfaceStability; | ||
| import org.apache.kafka.streams.processor.internals.RecordContext; | ||
|
|
||
| /** | ||
| * An interface that allows to dynamically determine the name of the Kafka topic to send at the sink node of the topology. | ||
| */ | ||
| @InterfaceStability.Evolving | ||
| public interface TopicNameExtractor<K, V> { | ||
|
|
||
| /** | ||
| * Extracts the topic name to send to. The topic name must be pre-existed, since the Kafka Streams library will not | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| * try to automatically create the topic with the extracted name. | ||
| * | ||
| * @param key the record key | ||
| * @param value the record value | ||
| * @param recordContext current context metadata of the record | ||
| * @return the topic name this record should be sent to | ||
| */ | ||
| String extract(K key, V value, RecordContext recordContext); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, "dynamically" need a little bit more explanation. Should we also state, that all those topics must be created by the user manually?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with "dynamically"... maybe "based on the {@code topicExtractor}"?
About the second point, I think that's addressed in the following line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to "based on the {@code topicExtractor}"