Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
80de5bb
[KIP-557] Add emit on change support
ConcurrencyPractitioner Mar 8, 2020
1aaaa01
Adding some class modifications
ConcurrencyPractitioner Mar 14, 2020
ec46f57
Adding class
ConcurrencyPractitioner Mar 14, 2020
2c19bf7
Merge branch 'trunk' into EMIT-ON-CHANGE
ConcurrencyPractitioner Mar 14, 2020
e3ffa93
Adding test
ConcurrencyPractitioner Mar 14, 2020
a14725a
Merge branch 'EMIT-ON-CHANGE' of https://github.com/ConcurrencyPracti…
ConcurrencyPractitioner Mar 14, 2020
6d3c7ab
Adding sensor
ConcurrencyPractitioner Mar 15, 2020
81c19bd
Add working sensor
ConcurrencyPractitioner Mar 15, 2020
209d376
Bumping processor level
ConcurrencyPractitioner Mar 15, 2020
8fef99c
Update streams/src/test/java/org/apache/kafka/streams/kstream/interna…
ConcurrencyPractitioner Apr 2, 2020
1d7c4ad
Update streams/src/main/java/org/apache/kafka/streams/kstream/interna…
ConcurrencyPractitioner Apr 2, 2020
d062bfa
Update streams/src/main/java/org/apache/kafka/streams/state/internals…
ConcurrencyPractitioner Apr 2, 2020
d0737f5
Fixing compilation errors and other issues
ConcurrencyPractitioner Apr 2, 2020
afdde96
Removing unneeded class
ConcurrencyPractitioner Apr 2, 2020
35c16b1
Removing all vestiges of new class
ConcurrencyPractitioner Apr 2, 2020
9e7649b
Adding more inclusive lambda
ConcurrencyPractitioner Apr 3, 2020
21a7345
Update streams/src/main/java/org/apache/kafka/streams/kstream/interna…
ConcurrencyPractitioner Apr 7, 2020
c4257e1
Adding some modifications
ConcurrencyPractitioner Apr 11, 2020
6ff8b65
Merge branch 'EMIT-ON-CHANGE' of https://github.com/ConcurrencyPracti…
ConcurrencyPractitioner Apr 11, 2020
860d41b
Pushing changes
ConcurrencyPractitioner Apr 11, 2020
2871c6b
Realiging diffs
ConcurrencyPractitioner Apr 11, 2020
367ebaf
Adding tests
ConcurrencyPractitioner Apr 11, 2020
449efd9
Adding test
ConcurrencyPractitioner Apr 13, 2020
9cd3726
Adding fixed getWithBinary TEst
ConcurrencyPractitioner Apr 15, 2020
cdad348
Fixing description
ConcurrencyPractitioner Apr 15, 2020
48cc4d9
Making some test changes
ConcurrencyPractitioner Apr 17, 2020
de0fc6d
Merge branch 'trunk' into EMIT-ON-CHANGE
ConcurrencyPractitioner Apr 23, 2020
d396fd4
Making some modifications to test
ConcurrencyPractitioner Apr 23, 2020
7d93107
Merge branch 'EMIT-ON-CHANGE' of https://github.com/ConcurrencyPracti…
ConcurrencyPractitioner Apr 23, 2020
07d40af
Merge branch 'trunk' of https://github.com/apache/kafka into EMIT-ON-…
ConcurrencyPractitioner Apr 24, 2020
4779b27
Fixing details
ConcurrencyPractitioner Apr 24, 2020
ddbf2cf
Catching bad modification
ConcurrencyPractitioner Apr 25, 2020
197ddd2
Resolving most comments
ConcurrencyPractitioner Apr 29, 2020
527ba28
Deleting massive amount of print statements
ConcurrencyPractitioner Apr 29, 2020
bae2860
Final removal
ConcurrencyPractitioner Apr 29, 2020
bf5532f
Resolving last comment
ConcurrencyPractitioner Apr 29, 2020
d9aa12d
Addressing some last comments
ConcurrencyPractitioner May 11, 2020
2a3d93d
minor test fix
vvcephei May 12, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;
import org.apache.kafka.streams.processor.ProcessorSupplier;
import org.apache.kafka.streams.processor.StateStore;
import org.apache.kafka.streams.processor.internals.InternalProcessorContext;
import org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl;
import org.apache.kafka.streams.state.TimestampedKeyValueStore;
import org.apache.kafka.streams.state.ValueAndTimestamp;
import org.apache.kafka.streams.state.internals.MeteredTimestampedKeyValueStore;
import org.apache.kafka.streams.state.internals.MeteredTimestampedKeyValueStore.RawAndDeserializedValue;
import org.apache.kafka.streams.state.internals.WrappedStateStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;

import static org.apache.kafka.streams.processor.internals.metrics.TaskMetrics.droppedRecordsSensorOrSkippedRecordsSensor;
import static org.apache.kafka.streams.processor.internals.metrics.ProcessorNodeMetrics.skippedIdempotentUpdatesSensor;

public class KTableSource<K, V> implements ProcessorSupplier<K, V> {
private static final Logger LOG = LoggerFactory.getLogger(KTableSource.class);
Expand Down Expand Up @@ -74,10 +79,11 @@ public boolean materialized() {

private class KTableSourceProcessor extends AbstractProcessor<K, V> {

private TimestampedKeyValueStore<K, V> store;
private MeteredTimestampedKeyValueStore<K, V> store;
private TimestampedTupleForwarder<K, V> tupleForwarder;
private StreamsMetricsImpl metrics;
private Sensor droppedRecordsSensor;
private Sensor skippedIdempotentUpdatesSensor = null;

@SuppressWarnings("unchecked")
@Override
Expand All @@ -86,12 +92,24 @@ public void init(final ProcessorContext context) {
metrics = (StreamsMetricsImpl) context.metrics();
droppedRecordsSensor = droppedRecordsSensorOrSkippedRecordsSensor(Thread.currentThread().getName(), context.taskId().toString(), metrics);
if (queryableName != null) {
store = (TimestampedKeyValueStore<K, V>) context.getStateStore(queryableName);
final StateStore stateStore = context.getStateStore(queryableName);
try {
store = ((WrappedStateStore<MeteredTimestampedKeyValueStore<K, V>, K, V>) stateStore).wrapped();
} catch (final ClassCastException e) {
throw new IllegalStateException("Unexpected store type: " + stateStore.getClass() + " for store: " + queryableName, e);
}
tupleForwarder = new TimestampedTupleForwarder<>(
store,
context,
new TimestampedCacheFlushListener<>(context),
sendOldValues);
skippedIdempotentUpdatesSensor = skippedIdempotentUpdatesSensor(
Thread.currentThread().getName(),
context.taskId().toString(),
((InternalProcessorContext) context).currentNode().name(),
metrics
);

}
}

Expand All @@ -108,7 +126,8 @@ public void process(final K key, final V value) {
}

if (queryableName != null) {
final ValueAndTimestamp<V> oldValueAndTimestamp = store.get(key);
final RawAndDeserializedValue<V> tuple = store.getWithBinary(key);
final ValueAndTimestamp<V> oldValueAndTimestamp = tuple.value;
final V oldValue;
if (oldValueAndTimestamp != null) {
oldValue = oldValueAndTimestamp.value();
Expand All @@ -119,8 +138,14 @@ public void process(final K key, final V value) {
} else {
oldValue = null;
}
store.put(key, ValueAndTimestamp.make(value, context().timestamp()));
tupleForwarder.maybeForward(key, value, oldValue);
final ValueAndTimestamp<V> newValueAndTimestamp = ValueAndTimestamp.make(value, context().timestamp());
final boolean isDifferentValue =
store.putIfDifferentValues(key, newValueAndTimestamp, tuple.serializedValue);
if (isDifferentValue) {
tupleForwarder.maybeForward(key, value, oldValue);
} else {
skippedIdempotentUpdatesSensor.record();
}
} else {
context().forward(key, new Change<>(value, null));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ private ProcessorNodeMetrics() {}
private static final String SUPPRESSION_EMIT_RATE_DESCRIPTION =
RATE_DESCRIPTION_PREFIX + SUPPRESSION_EMIT_DESCRIPTION + RATE_DESCRIPTION_SUFFIX;

private static final String IDEMPOTENT_UPDATE_SKIP = "idempotent-update-skip";
private static final String IDEMPOTENT_UPDATE_SKIP_DESCRIPTION = "skipped idempotent updates";
private static final String IDEMPOTENT_UPDATE_SKIP_TOTAL_DESCRIPTION = TOTAL_DESCRIPTION + IDEMPOTENT_UPDATE_SKIP_DESCRIPTION;
private static final String IDEMPOTENT_UPDATE_SKIP_RATE_DESCRIPTION =
RATE_DESCRIPTION_PREFIX + IDEMPOTENT_UPDATE_SKIP_DESCRIPTION + RATE_DESCRIPTION_SUFFIX;

private static final String PROCESS = "process";
private static final String PROCESS_DESCRIPTION = "calls to process";
private static final String PROCESS_TOTAL_DESCRIPTION = TOTAL_DESCRIPTION + PROCESS_DESCRIPTION;
Expand Down Expand Up @@ -108,6 +114,22 @@ public static Sensor suppressionEmitSensor(final String threadId,
);
}

public static Sensor skippedIdempotentUpdatesSensor(final String threadId,
final String taskId,
final String processorNodeId,
final StreamsMetricsImpl streamsMetrics) {
return throughputSensor(
threadId,
taskId,
processorNodeId,
IDEMPOTENT_UPDATE_SKIP,
IDEMPOTENT_UPDATE_SKIP_RATE_DESCRIPTION,
IDEMPOTENT_UPDATE_SKIP_TOTAL_DESCRIPTION,
RecordingLevel.DEBUG,
streamsMetrics
);
}

Comment on lines +117 to +132

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req: Please add a unit test in ProcessorNodeMetricsTest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

public static Sensor processSensor(final String threadId,
final String taskId,
final String processorNodeId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ public class MeteredKeyValueStore<K, V>

private final String metricsScope;
protected final Time time;
private Sensor putSensor;
protected Sensor putSensor;
private Sensor putIfAbsentSensor;
private Sensor getSensor;
protected Sensor getSensor;
private Sensor deleteSensor;
private Sensor putAllSensor;
private Sensor allSensor;
Expand Down Expand Up @@ -206,11 +206,11 @@ public void close() {
}
}

private V outerValue(final byte[] value) {
protected V outerValue(final byte[] value) {
return value != null ? serdes.valueFrom(value) : null;
}

private Bytes keyBytes(final K key) {
protected Bytes keyBytes(final K key) {
return Bytes.wrap(serdes.rawKey(key));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
*/
package org.apache.kafka.streams.state.internals;

import static org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl.maybeMeasureLatency;

import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.streams.errors.ProcessorStateException;
import org.apache.kafka.streams.processor.ProcessorContext;
import org.apache.kafka.streams.processor.internals.ProcessorStateManager;
import org.apache.kafka.streams.state.KeyValueStore;
Expand All @@ -35,7 +38,7 @@
* @param <V>
*/
public class MeteredTimestampedKeyValueStore<K, V>
extends MeteredKeyValueStore<K, ValueAndTimestamp<V>>
extends MeteredKeyValueStore<K, ValueAndTimestamp<V>>
implements TimestampedKeyValueStore<K, V> {

MeteredTimestampedKeyValueStore(final KeyValueStore<Bytes, byte[]> inner,
Expand All @@ -53,4 +56,48 @@ void initStoreSerde(final ProcessorContext context) {
keySerde == null ? (Serde<K>) context.keySerde() : keySerde,
valueSerde == null ? new ValueAndTimestampSerde<>((Serde<V>) context.valueSerde()) : valueSerde);
}
}

public RawAndDeserializedValue<V> getWithBinary(final K key) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req: Please add unit tests for this method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

try {
return maybeMeasureLatency(() -> {
final byte[] serializedValue = wrapped().get(keyBytes(key));
return new RawAndDeserializedValue<V>(serializedValue, outerValue(serializedValue));
}, time, getSensor);
} catch (final ProcessorStateException e) {
final String message = String.format(e.getMessage(), key);
throw new ProcessorStateException(message, e);
}
}

public boolean putIfDifferentValues(final K key,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req: Please add unit tests for this method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

final ValueAndTimestamp<V> newValue,
final byte[] oldSerializedValue) {
try {
return maybeMeasureLatency(
() -> {
final byte[] newSerializedValue = serdes.rawValue(newValue);
if (ValueAndTimestampSerializer.compareValuesAndCheckForIncreasingTimestamp(oldSerializedValue, newSerializedValue)) {
return false;
} else {
wrapped().put(keyBytes(key), newSerializedValue);

@rodesai rodesai May 14, 2020

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ConcurrencyPractitioner @vvcephei I'm trying to understand this to debug some broken tests in ksql. Couple questions:

When the timestamp of the newer value is lower (ignoring the value), why do we want to put the new value into the store? Surely the store should have the value with the newer timestamp? Otherwise we could wind up with a corrupt store.

Don't we still want to put the value in the store (even if we don't forward it on to the next context) if the values are the same but the timestamp is newer? Otherwise if we get an out-of-order update with a different value, but a timestamp in between the rows with the same value, we'd incorrectly put that value into the store, e.g. the following updates:

TS: 1, K: X, V: A
TS: 3, K: X, V: A
TS: 2, K: X, V: B

would result in the table containing K: X, V: B, which is wrong.

@cadonna cadonna May 14, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why when the timestamp of the newer value is lower, do we want to put the new value into the store? Surely the store should have the value with the newer timestamp? Otherwise we could wind up with a corrupt store.

This behavior was there also before this PR. If a out-of-order record is encountered, a log message was written, but the record was nevertheless put into the state store (cf.

store.put(key, ValueAndTimestamp.make(value, context().timestamp()));
). The only thing that changed is that if the serialized value of the new record is equal to the serialized value of the old value and the timestamp of the new record is equal or newer, we drop the record because it is a idempotent update.
Could you elaborate on why a store should get corrupted because of this?

would result in the table containing K: X, V: B, which is wrong.

As said above, this behavior should not have been changed.

@cadonna cadonna May 14, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we still want to put the value in the store (even if we don't forward it on to the next context) if the values are the same but the timestamp is newer?

If we just put the value in the store but did not forward it, then the store would actually be corrupted, because the local state would not be consistent with downstream anymore.

Not putting a record with the same value but a newer timestamp in the store and not forwarding it was the main point of this KIP.

return true;
}
},
time,
putSensor
);
} catch (final ProcessorStateException e) {
final String message = String.format(e.getMessage(), key, newValue);
throw new ProcessorStateException(message, e);
}
}

public static class RawAndDeserializedValue<ValueType> {
public final byte[] serializedValue;
public final ValueAndTimestamp<ValueType> value;
public RawAndDeserializedValue(final byte[] serializedValue, final ValueAndTimestamp<ValueType> value) {
this.serializedValue = serializedValue;
this.value = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,51 @@ public class ValueAndTimestampSerializer<V> implements Serializer<ValueAndTimest
timestampSerializer = new LongSerializer();
}

private static boolean skipTimestampAndCompareValues(final byte[] left, final byte[] right) {
for (int i = Long.BYTES; i < left.length; i++) {
if (left[i] != right[i]) {
return false;
}
}
return true;
}

private static long extractTimestamp(final byte[] bytes) {
final byte[] timestampBytes = new byte[Long.BYTES];
for (int i = 0; i < Long.BYTES; i++) {
timestampBytes[i] = bytes[i];
}
return ByteBuffer.wrap(timestampBytes).getLong();
}

/**
* @param left the serialized byte array of the old record in state store
* @param right the serialized byte array of the new record being processed
* @return true if the two serialized values are the same (excluding timestamp) or
* if the timestamp of right is less than left (indicating out of order record)
* false otherwise
*/
public static boolean compareValuesAndCheckForIncreasingTimestamp(final byte[] left, final byte[] right) {
if (left == right) {
return true;
}
if (left == null || right == null) {
return false;
}

final int length = left.length;
if (right.length != length) {
return false;
}

final long leftTimestamp = extractTimestamp(left);
final long rightTimestamp = extractTimestamp(right);
if (rightTimestamp < leftTimestamp) {
return false;
}
return skipTimestampAndCompareValues(left, right);
}

@Override
public void configure(final Map<String, ?> configs,
final boolean isKey) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import static java.util.Collections.singletonMap;


import static java.util.Collections.emptyMap;
import static org.apache.kafka.common.utils.Utils.mkEntry;
Expand Down Expand Up @@ -384,12 +386,16 @@ public void shouldEmitTombstoneWhenDeletingNonJoiningRecords() {

// Deleting a non-joining record produces an unnecessary tombstone for inner joins, because
// it's not possible to know whether a result was previously emitted.
// HOWEVER, when the final join result is materialized (either explicitly or
// implicitly by a subsequent join), we _can_ detect that the tombstone is unnecessary and drop it.
// For the left join, the tombstone is necessary.
left.pipeInput("lhs1", (String) null);
{
assertThat(
outputTopic.readKeyValuesToMap(),
is(mkMap(mkEntry("lhs1", null)))
is(leftJoin || !(materialized || rejoin)
? mkMap(mkEntry("lhs1", null))
: emptyMap())
);
if (materialized) {
assertThat(
Expand Down Expand Up @@ -466,11 +472,15 @@ public void joinShouldProduceNullsWhenValueHasNonMatchingForeignKey() {
// "moving" our subscription to another non-existent FK results in an unnecessary tombstone for inner join,
// since it impossible to know whether the prior FK existed or not (and thus whether any results have
// previously been emitted)
// previously been emitted). HOWEVER, when the final join result is materialized (either explicitly or
// implicitly by a subsequent join), we _can_ detect that the tombstone is unnecessary and drop it.
// The left join emits a _necessary_ update (since the lhs record has actually changed)
left.pipeInput("lhs1", "lhsValue1|rhs2");
assertThat(
outputTopic.readKeyValuesToMap(),
is(mkMap(mkEntry("lhs1", leftJoin ? "(lhsValue1|rhs2,null)" : null)))
is(leftJoin
? mkMap(mkEntry("lhs1", "(lhsValue1|rhs2,null)"))
: (materialized || rejoin) ? emptyMap() : singletonMap("lhs1", null))
);
if (materialized) {
assertThat(
Expand All @@ -482,7 +492,9 @@ public void joinShouldProduceNullsWhenValueHasNonMatchingForeignKey() {
left.pipeInput("lhs1", "lhsValue1|rhs3");
assertThat(
outputTopic.readKeyValuesToMap(),
is(mkMap(mkEntry("lhs1", leftJoin ? "(lhsValue1|rhs3,null)" : null)))
is(leftJoin
? mkMap(mkEntry("lhs1", "(lhsValue1|rhs3,null)"))
: (materialized || rejoin) ? emptyMap() : singletonMap("lhs1", null))
);
if (materialized) {
assertThat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public void before() {
@After
public void after() throws Exception {
IntegrationTestUtils.purgeLocalStreamsState(STREAMS_CONFIG);
CLUSTER.deleteAllTopicsAndWait(60000L);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public void shouldRestoreStateFromSourceTopic() throws Exception {
// restoring from 1000 to 4000 (committed), and then process from 4000 to 5000 on each of the two partitions
final int offsetLimitDelta = 1000;
final int offsetCheckpointed = 1000;
createStateForRestoration(INPUT_STREAM);
createStateForRestoration(INPUT_STREAM, 0);
setCommittedOffset(INPUT_STREAM, offsetLimitDelta);

final StateDirectory stateDirectory = new StateDirectory(new StreamsConfig(props), new MockTime(), true);
Expand All @@ -143,7 +143,7 @@ public void shouldRestoreStateFromSourceTopic() throws Exception {
builder.table(INPUT_STREAM, Materialized.<Integer, Integer, KeyValueStore<Bytes, byte[]>>as("store").withKeySerde(Serdes.Integer()).withValueSerde(Serdes.Integer()))
.toStream()
.foreach((key, value) -> {
if (numReceived.incrementAndGet() == 2 * offsetLimitDelta) {
if (numReceived.incrementAndGet() == offsetLimitDelta * 2) {
shutdownLatch.countDown();
}
});
Expand Down Expand Up @@ -190,8 +190,8 @@ public void shouldRestoreStateFromChangelogTopic() throws Exception {

// restoring from 1000 to 5000, and then process from 5000 to 10000 on each of the two partitions
final int offsetCheckpointed = 1000;
createStateForRestoration(APPID + "-store-changelog");
createStateForRestoration(INPUT_STREAM);
createStateForRestoration(APPID + "-store-changelog", 0);
createStateForRestoration(INPUT_STREAM, 10000);

final StateDirectory stateDirectory = new StateDirectory(new StreamsConfig(props), new MockTime(), true);
// note here the checkpointed offset is the last processed record's offset, so without control message we should write this offset - 1
Expand Down Expand Up @@ -345,15 +345,16 @@ public void process(final Integer key, final Integer value) {
public void close() { }
}

private void createStateForRestoration(final String changelogTopic) {
private void createStateForRestoration(final String changelogTopic, final int startingOffset) {
final Properties producerConfig = new Properties();
producerConfig.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.bootstrapServers());

try (final KafkaProducer<Integer, Integer> producer =
new KafkaProducer<>(producerConfig, new IntegerSerializer(), new IntegerSerializer())) {

for (int i = 0; i < numberOfKeys; i++) {
producer.send(new ProducerRecord<>(changelogTopic, i, i));
final int offset = startingOffset + i;
producer.send(new ProducerRecord<>(changelogTopic, offset, offset));
}
}
}
Expand Down
Loading