Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/streams/developer-guide/processor-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<li><a class="reference internal" href="#enable-or-disable-fault-tolerance-of-state-stores-store-changelogs" id="id6">Enable or Disable Fault Tolerance of State Stores (Store Changelogs)</a></li>
<li><a class="reference internal" href="#timestamped-state-stores" id="id11">Timestamped State Stores</a></li>
<li><a class="reference internal" href="#versioned-state-stores" id="id12">Versioned Key-Value State Stores</a></li>
<li><a class="reference internal" href="#readonly-state-stores" id="id12">Readonly State Stores</a></li>
<li><a class="reference internal" href="#implementing-custom-state-stores" id="id7">Implementing Custom State Stores</a></li>
</ul>
</li>
Expand Down Expand Up @@ -466,6 +467,18 @@ <h2>
stores to rebuild state from changelog.</li>
</ul>
</p>
<div class="section" id="readonly-state-stores">
<span id="streams-developer-guide-state-store-readonly"></span><h3><a class="toc-backref" href="#id12">ReadOnly State Stores</a><a class="headerlink" href="#readonly-state-stores" title="Permalink to this headline"></a></h3>
<p>A read-only state store materialized the data from its input topic. It also uses the input topic
for fault-tolerance, and thus does not have an additional changelog topic (the input topic is
re-used as changelog). Thus, the input topic should be configured with <a class="reference external" href="https://kafka.apache.org/documentation.html#compaction">log compaction</a>.
Note that no other processor should modify the content of the state store, and the only writer
should be the associated "state update processor"; other processors may read the content of the
read-only store.</p>

<p><b>note:</b> beware of the partitioning requirements when using read-only state stores for lookups during
processing. You might want to make sure the original changelog topic is co-partitioned with the processors
reading the read-only statestore.</p>
</div>
<div class="section" id="implementing-custom-state-stores">
<span id="streams-developer-guide-state-store-custom"></span><h3><a class="toc-backref" href="#id7">Implementing Custom State Stores</a><a class="headerlink" href="#implementing-custom-state-stores" title="Permalink to this headline"></a></h3>
Expand Down
8 changes: 8 additions & 0 deletions docs/streams/upgrade-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ <h3 class="anchor-heading"><a id="streams_notable_changes" class="anchor-link"><
More details about the new config <code>StreamsConfig#TOPOLOGY_OPTIMIZATION</code> can be found in <a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-295%3A+Add+Streams+Configuration+Allowing+for+Optional+Topology+Optimization">KIP-295</a>.
</p>

<h4><a id="streams_api_changes_380" href="#streams_api_changes_380">Streams API changes in 3.8.0</a></h3>
<p>
The Processor API now support so-called read-only state stores, added via
<a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-813%3A+Shareable+State+Stores">KIP-813</a>.
These stores don't have a dedicated changelog topic, but use their source topic for fault-tolerance,
simlar to <code>KTables</code> with source-topic optimization enabled.
</p>

<h3><a id="streams_api_changes_370" href="#streams_api_changes_370">Streams API changes in 3.7.0</a></h3>
<p>
We added a new method to <code>KafkaStreams</code>, namely <code>KafkaStreams#setStandbyUpdateListener()</code> in
Expand Down
82 changes: 82 additions & 0 deletions streams/src/main/java/org/apache/kafka/streams/Topology.java
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,88 @@ public synchronized Topology addStateStore(final StoreBuilder<?> storeBuilder,
return this;
}

/**
* Adds a read-only {@link StateStore} to the topology.
* <p>
* A read-only {@link StateStore} does not create a dedicated changelog topic but uses it's input topic as
* changelog; thus, the used topic should be configured with log compaction.
* <p>
* The <code>auto.offset.reset</code> property will be set to <code>earliest</code> for this topic.
* <p>
* The provided {@link ProcessorSupplier} will be used to create a processor for all messages received
* from the given topic. This processor should contain logic to keep the {@link StateStore} up-to-date.
*
* @param storeBuilder user defined store builder
* @param sourceName name of the {@link SourceNode} that will be automatically added
Comment thread
calmera marked this conversation as resolved.
* @param timestampExtractor the stateless timestamp extractor used for this source,
* if not specified the default extractor defined in the configs will be used
* @param keyDeserializer the {@link Deserializer} to deserialize keys with
* @param valueDeserializer the {@link Deserializer} to deserialize values with
* @param topic the topic to source the data from
* @param processorName the name of the {@link ProcessorSupplier}
* @param stateUpdateSupplier the instance of {@link ProcessorSupplier}
* @return itself
* @throws TopologyException if the processor of state is already registered
*/
Comment thread
calmera marked this conversation as resolved.
public synchronized <KIn, VIn> Topology addReadOnlyStateStore(final StoreBuilder<?> storeBuilder,
final String sourceName,
final TimestampExtractor timestampExtractor,
final Deserializer<KIn> keyDeserializer,
final Deserializer<VIn> valueDeserializer,
final String topic,
final String processorName,
final ProcessorSupplier<KIn, VIn, Void, Void> stateUpdateSupplier) {
storeBuilder.withLoggingDisabled();

internalTopologyBuilder.addSource(AutoOffsetReset.EARLIEST, sourceName, timestampExtractor, keyDeserializer, valueDeserializer, topic);
internalTopologyBuilder.addProcessor(processorName, stateUpdateSupplier, sourceName);
internalTopologyBuilder.addStateStore(storeBuilder, processorName);
internalTopologyBuilder.connectSourceStoreAndTopic(storeBuilder.name(), topic);

return this;
}

/**
* Adds a read-only {@link StateStore} to the topology.
* <p>
* A read-only {@link StateStore} does not create a dedicated changelog topic but uses it's input topic as
* changelog; thus, the used topic should be configured with log compaction.
* <p>
* The <code>auto.offset.reset</code> property will be set to <code>earliest</code> for this topic.
* <p>
* The provided {@link ProcessorSupplier} will be used to create a processor for all messages received
* from the given topic. This processor should contain logic to keep the {@link StateStore} up-to-date.
* The default {@link TimestampExtractor} as specified in the {@link StreamsConfig config} is used.
*
* @param storeBuilder user defined store builder
* @param sourceName name of the {@link SourceNode} that will be automatically added
* @param keyDeserializer the {@link Deserializer} to deserialize keys with
* @param valueDeserializer the {@link Deserializer} to deserialize values with
* @param topic the topic to source the data from
* @param processorName the name of the {@link ProcessorSupplier}
* @param stateUpdateSupplier the instance of {@link ProcessorSupplier}
* @return itself
* @throws TopologyException if the processor of state is already registered
*/
public synchronized <KIn, VIn> Topology addReadOnlyStateStore(final StoreBuilder<?> storeBuilder,
final String sourceName,
final Deserializer<KIn> keyDeserializer,
final Deserializer<VIn> valueDeserializer,
final String topic,
final String processorName,
final ProcessorSupplier<KIn, VIn, Void, Void> stateUpdateSupplier) {
return addReadOnlyStateStore(
storeBuilder,
sourceName,
null,
keyDeserializer,
valueDeserializer,
topic,
processorName,
stateUpdateSupplier
);
}

/**
* Adds a global {@link StateStore} to the topology.
* The {@link StateStore} sources its data from all partitions of the provided input topic.
Expand Down
63 changes: 61 additions & 2 deletions streams/src/test/java/org/apache/kafka/streams/TopologyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.kafka.streams.processor.internals.InternalTopologyBuilder;
import org.apache.kafka.streams.processor.internals.InternalTopologyBuilder.SubtopologyDescription;
import org.apache.kafka.streams.processor.internals.ProcessorTopology;
import org.apache.kafka.streams.processor.internals.StoreFactory;
import org.apache.kafka.streams.state.KeyValueStore;
import org.apache.kafka.streams.state.SessionStore;
import org.apache.kafka.streams.state.StoreBuilder;
Expand All @@ -58,6 +59,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.internal.util.collections.Sets;

import java.time.Duration;
import java.util.Arrays;
Expand Down Expand Up @@ -337,7 +339,7 @@ public void shouldNotAllowToAddStoreWithSameNameAndDifferentInstance() {
mockStoreBuilder();
topology.addStateStore(storeBuilder);

final StoreBuilder otherStoreBuilder = mock(StoreBuilder.class);
final StoreBuilder<?> otherStoreBuilder = mock(StoreBuilder.class);
when(otherStoreBuilder.name()).thenReturn("store");
when(otherStoreBuilder.logConfig()).thenReturn(Collections.emptyMap());
when(otherStoreBuilder.loggingEnabled()).thenReturn(false);
Expand Down Expand Up @@ -2313,7 +2315,7 @@ private TopologyDescription.Sink addSink(final String sinkName,

topology.addSink(sinkName, sinkTopic, null, null, null, parentNames);
final TopologyDescription.Sink expectedSinkNode =
new InternalTopologyBuilder.Sink(sinkName, sinkTopic);
new InternalTopologyBuilder.Sink<>(sinkName, sinkTopic);

for (final TopologyDescription.Node parent : parents) {
((InternalTopologyBuilder.AbstractNode) parent).addSuccessor(expectedSinkNode);
Expand Down Expand Up @@ -2351,6 +2353,63 @@ private void addGlobalStoreToTopologyAndExpectedDescription(final String globalS
expectedDescription.addGlobalStore(expectedGlobalStore);
}

@Test
public void readOnlyStateStoresShouldHaveTheirOwnSubTopology() {
final String sourceName = "source";
final String storeName = "store";
final String topicName = "topic";
final String processorName = "processor";

final KeyValueStoreBuilder<?, ?> storeBuilder = mock(KeyValueStoreBuilder.class);
when(storeBuilder.name()).thenReturn(storeName);
topology.addReadOnlyStateStore(
storeBuilder,
sourceName,
null,
null,
null,
topicName,
processorName,
new MockProcessorSupplier<>());

final TopologyDescription.Source expectedSource = new InternalTopologyBuilder.Source(sourceName, Sets.newSet(topicName), null);
final TopologyDescription.Processor expectedProcessor = new InternalTopologyBuilder.Processor(processorName, Sets.newSet(storeName));

((InternalTopologyBuilder.AbstractNode) expectedSource).addSuccessor(expectedProcessor);
((InternalTopologyBuilder.AbstractNode) expectedProcessor).addPredecessor(expectedSource);

final Set<TopologyDescription.Node> allNodes = new HashSet<>();
allNodes.add(expectedSource);
allNodes.add(expectedProcessor);
expectedDescription.addSubtopology(new SubtopologyDescription(0, allNodes));

assertThat(topology.describe(), equalTo(expectedDescription));
assertThat(topology.describe().hashCode(), equalTo(expectedDescription.hashCode()));
}

@Test
public void readOnlyStateStoresShouldNotLog() {
final String sourceName = "source";
final String storeName = "store";
final String topicName = "topic";
final String processorName = "processor";

final KeyValueStoreBuilder<?, ?> storeBuilder = mock(KeyValueStoreBuilder.class);
when(storeBuilder.name()).thenReturn(storeName);
topology.addReadOnlyStateStore(
storeBuilder,
sourceName,
null,
null,
null,
topicName,
processorName,
new MockProcessorSupplier<>());

final StoreFactory stateStoreFactory = topology.internalTopologyBuilder.stateStores().get(storeName);
assertThat(stateStoreFactory.loggingEnabled(), equalTo(false));
}

private TopologyConfig overrideDefaultStore(final String defaultStore) {
final Properties topologyOverrides = new Properties();
// change default store as in-memory
Expand Down
Loading