diff --git a/checkstyle/suppressions.xml b/checkstyle/suppressions.xml
index 4c1cbce02333b..cf7ff87eef001 100644
--- a/checkstyle/suppressions.xml
+++ b/checkstyle/suppressions.xml
@@ -230,19 +230,19 @@
files=".*[/\\]streams[/\\].*test[/\\].*.java"/>
+ files="(EosV2UpgradeIntegrationTest|KStreamKStreamJoinTest|KTableKTableForeignKeyJoinIntegrationTest|KTableKTableForeignKeyVersionedJoinIntegrationTest|RocksDBGenericOptionsToDbOptionsColumnFamilyOptionsAdapterTest|RelationalSmokeTest|MockProcessorContextStateStoreTest).java"/>
+ files="(EosV2UpgradeIntegrationTest|EosTestDriver|KStreamKStreamJoinTest|KTableKTableForeignKeyJoinIntegrationTest|KTableKTableForeignKeyVersionedJoinIntegrationTest|RelationalSmokeTest|MockProcessorContextStateStoreTest|TopologyTestDriverTest).java"/>
+ files="(KStreamSlidingWindowAggregateTest|KStreamKStreamLeftJoinTest|KStreamKStreamOuterJoinTest|KTableKTableForeignKeyVersionedJoinIntegrationTest).java"/>
diff --git a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/KTableImpl.java b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/KTableImpl.java
index cd8c5abc280d1..82438ff59a6b8 100644
--- a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/KTableImpl.java
+++ b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/KTableImpl.java
@@ -1116,7 +1116,8 @@ private KTable doJoinOnForeignKey(final KTable forei
keySerde
);
- final ProcessorGraphNode> subscriptionNode = new ProcessorGraphNode<>(
+ final KTableValueGetterSupplier primaryKeyValueGetter = valueGetterSupplier();
+ final StatefulProcessorNode> subscriptionNode = new StatefulProcessorNode<>(
new ProcessorParameters<>(
new ForeignJoinSubscriptionSendProcessorSupplier<>(
foreignKeyExtractor,
@@ -1124,10 +1125,13 @@ private KTable doJoinOnForeignKey(final KTable forei
valueHashSerdePseudoTopic,
foreignKeySerde,
valueSerde == null ? null : valueSerde.serializer(),
- leftJoin
+ leftJoin,
+ primaryKeyValueGetter
),
renamed.suffixWithOrElseGet("-subscription-registration-processor", builder, SUBSCRIPTION_REGISTRATION)
- )
+ ),
+ Collections.emptySet(),
+ Collections.singleton(primaryKeyValueGetter)
);
builder.addGraphNode(graphNode, subscriptionNode);
@@ -1179,26 +1183,27 @@ private KTable doJoinOnForeignKey(final KTable forei
);
builder.addGraphNode(subscriptionSource, subscriptionReceiveNode);
+ final KTableValueGetterSupplier foreignKeyValueGetter = ((KTableImpl) foreignKeyTable).valueGetterSupplier();
final StatefulProcessorNode, Change>>> subscriptionJoinForeignNode =
new StatefulProcessorNode<>(
new ProcessorParameters<>(
new SubscriptionJoinForeignProcessorSupplier<>(
- ((KTableImpl) foreignKeyTable).valueGetterSupplier()
+ foreignKeyValueGetter
),
renamed.suffixWithOrElseGet("-subscription-join-foreign", builder, SUBSCRIPTION_PROCESSOR)
),
Collections.emptySet(),
- Collections.singleton(((KTableImpl) foreignKeyTable).valueGetterSupplier())
+ Collections.singleton(foreignKeyValueGetter)
);
builder.addGraphNode(subscriptionReceiveNode, subscriptionJoinForeignNode);
- final StatefulProcessorNode> foreignJoinSubscriptionNode = new StatefulProcessorNode<>(
+ final StatefulProcessorNode> foreignJoinSubscriptionNode = new StatefulProcessorNode<>(
new ProcessorParameters<>(
- new ForeignJoinSubscriptionProcessorSupplier<>(subscriptionStore, combinedKeySchema),
+ new ForeignJoinSubscriptionProcessorSupplier<>(subscriptionStore, combinedKeySchema, foreignKeyValueGetter),
renamed.suffixWithOrElseGet("-foreign-join-subscription", builder, SUBSCRIPTION_PROCESSOR)
),
Collections.singleton(subscriptionStore),
- Collections.emptySet()
+ Collections.singleton(foreignKeyValueGetter)
);
builder.addGraphNode(((KTableImpl) foreignKeyTable).graphNode, foreignJoinSubscriptionNode);
@@ -1232,7 +1237,6 @@ private KTable doJoinOnForeignKey(final KTable forei
resultSourceNodes.add(foreignResponseSource.nodeName());
builder.internalTopologyBuilder.copartitionSources(resultSourceNodes);
- final KTableValueGetterSupplier primaryKeyValueGetter = valueGetterSupplier();
final SubscriptionResolverJoinProcessorSupplier resolverProcessorSupplier = new SubscriptionResolverJoinProcessorSupplier<>(
primaryKeyValueGetter,
valueSerde == null ? null : valueSerde.serializer(),
diff --git a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionProcessorSupplier.java b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionProcessorSupplier.java
index 55e40fce64f18..46e2bd24c2577 100644
--- a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionProcessorSupplier.java
+++ b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionProcessorSupplier.java
@@ -21,6 +21,8 @@
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.kstream.internals.Change;
+import org.apache.kafka.streams.kstream.internals.KTableValueGetter;
+import org.apache.kafka.streams.kstream.internals.KTableValueGetterSupplier;
import org.apache.kafka.streams.processor.api.ContextualProcessor;
import org.apache.kafka.streams.processor.api.Processor;
import org.apache.kafka.streams.processor.api.ProcessorContext;
@@ -43,24 +45,32 @@ public class ForeignJoinSubscriptionProcessorSupplier implements
private static final Logger LOG = LoggerFactory.getLogger(ForeignJoinSubscriptionProcessorSupplier.class);
private final StoreBuilder>> storeBuilder;
private final CombinedKeySchema keySchema;
+ private final KTableValueGetterSupplier foreignKeyValueGetterSupplier;
public ForeignJoinSubscriptionProcessorSupplier(
final StoreBuilder>> storeBuilder,
- final CombinedKeySchema keySchema) {
+ final CombinedKeySchema keySchema,
+ final KTableValueGetterSupplier foreignKeyValueGetterSupplier) {
this.storeBuilder = storeBuilder;
this.keySchema = keySchema;
+ this.foreignKeyValueGetterSupplier = foreignKeyValueGetterSupplier;
}
@Override
public Processor, K, SubscriptionResponseWrapper> get() {
- return new KTableKTableJoinProcessor();
+ return new KTableKTableJoinProcessor(foreignKeyValueGetterSupplier.get());
}
private final class KTableKTableJoinProcessor extends ContextualProcessor, K, SubscriptionResponseWrapper> {
private Sensor droppedRecordsSensor;
- private TimestampedKeyValueStore> store;
+ private TimestampedKeyValueStore> subscriptionStore;
+ private final KTableValueGetter foreignKeyValueGetter;
+
+ private KTableKTableJoinProcessor(final KTableValueGetter foreignKeyValueGetter) {
+ this.foreignKeyValueGetter = foreignKeyValueGetter;
+ }
@Override
public void init(final ProcessorContext> context) {
@@ -71,7 +81,8 @@ public void init(final ProcessorContext> cont
internalProcessorContext.taskId().toString(),
internalProcessorContext.metrics()
);
- store = internalProcessorContext.getStateStore(storeBuilder);
+ subscriptionStore = internalProcessorContext.getStateStore(storeBuilder);
+ foreignKeyValueGetter.init(context);
}
@Override
@@ -95,11 +106,21 @@ public void process(final Record> record) {
return;
}
+ // drop out-of-order records from versioned tables (cf. KIP-914)
+ if (foreignKeyValueGetter.isVersioned()) {
+ final ValueAndTimestamp latestValueAndTimestamp = foreignKeyValueGetter.get(record.key());
+ if (latestValueAndTimestamp != null && latestValueAndTimestamp.timestamp() > record.timestamp()) {
+ LOG.info("Skipping out-of-order record from versioned table while performing table-table join.");
+ droppedRecordsSensor.record();
+ return;
+ }
+ }
+
final Bytes prefixBytes = keySchema.prefixBytes(record.key());
//Perform the prefixScan and propagate the results
try (final KeyValueIterator>> prefixScanResults =
- store.range(prefixBytes, Bytes.increment(prefixBytes))) {
+ subscriptionStore.range(prefixBytes, Bytes.increment(prefixBytes))) {
while (prefixScanResults.hasNext()) {
final KeyValue>> next = prefixScanResults.next();
@@ -118,6 +139,11 @@ public void process(final Record> record) {
}
}
+ @Override
+ public void close() {
+ foreignKeyValueGetter.close();
+ }
+
private boolean prefixEquals(final byte[] x, final byte[] y) {
final int min = Math.min(x.length, y.length);
final ByteBuffer xSlice = ByteBuffer.wrap(x, 0, min);
diff --git a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionSendProcessorSupplier.java b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionSendProcessorSupplier.java
index 0efe4da2bcbf9..3d8b5dd222ea8 100644
--- a/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionSendProcessorSupplier.java
+++ b/streams/src/main/java/org/apache/kafka/streams/kstream/internals/foreignkeyjoin/ForeignJoinSubscriptionSendProcessorSupplier.java
@@ -21,6 +21,8 @@
import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.serialization.Serializer;
import org.apache.kafka.streams.kstream.internals.Change;
+import org.apache.kafka.streams.kstream.internals.KTableValueGetter;
+import org.apache.kafka.streams.kstream.internals.KTableValueGetterSupplier;
import org.apache.kafka.streams.processor.api.ContextualProcessor;
import org.apache.kafka.streams.processor.api.Processor;
import org.apache.kafka.streams.processor.api.ProcessorContext;
@@ -29,6 +31,7 @@
import org.apache.kafka.streams.processor.api.RecordMetadata;
import org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl;
import org.apache.kafka.streams.processor.internals.metrics.TaskMetrics;
+import org.apache.kafka.streams.state.ValueAndTimestamp;
import org.apache.kafka.streams.state.internals.Murmur3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,6 +52,7 @@ public class ForeignJoinSubscriptionSendProcessorSupplier implements P
private final Supplier foreignKeySerdeTopicSupplier;
private final Supplier valueSerdeTopicSupplier;
private final boolean leftJoin;
+ private final KTableValueGetterSupplier primaryKeyValueGetterSupplier;
private Serializer foreignKeySerializer;
private Serializer valueSerializer;
@@ -57,18 +61,20 @@ public ForeignJoinSubscriptionSendProcessorSupplier(final Function foreig
final Supplier valueSerdeTopicSupplier,
final Serde foreignKeySerde,
final Serializer valueSerializer,
- final boolean leftJoin) {
+ final boolean leftJoin,
+ final KTableValueGetterSupplier primaryKeyValueGetterSupplier) {
this.foreignKeyExtractor = foreignKeyExtractor;
this.foreignKeySerdeTopicSupplier = foreignKeySerdeTopicSupplier;
this.valueSerdeTopicSupplier = valueSerdeTopicSupplier;
this.valueSerializer = valueSerializer;
this.leftJoin = leftJoin;
+ this.primaryKeyValueGetterSupplier = primaryKeyValueGetterSupplier;
foreignKeySerializer = foreignKeySerde == null ? null : foreignKeySerde.serializer();
}
@Override
public Processor, KO, SubscriptionWrapper> get() {
- return new UnbindChangeProcessor();
+ return new UnbindChangeProcessor(primaryKeyValueGetterSupplier.get());
}
private class UnbindChangeProcessor extends ContextualProcessor, KO, SubscriptionWrapper> {
@@ -76,6 +82,11 @@ private class UnbindChangeProcessor extends ContextualProcessor, KO
private Sensor droppedRecordsSensor;
private String foreignKeySerdeTopic;
private String valueSerdeTopic;
+ private final KTableValueGetter primaryKeyValueGetter;
+
+ private UnbindChangeProcessor(final KTableValueGetter primaryKeyValueGetter) {
+ this.primaryKeyValueGetter = primaryKeyValueGetter;
+ }
@SuppressWarnings("unchecked")
@Override
@@ -95,10 +106,25 @@ public void init(final ProcessorContext> context) {
context.taskId().toString(),
(StreamsMetricsImpl) context.metrics()
);
+ primaryKeyValueGetter.init(context);
}
@Override
public void process(final Record> record) {
+ // drop out-of-order records from versioned tables (cf. KIP-914)
+ if (primaryKeyValueGetter.isVersioned()) {
+ // key-value stores do not contain data for null keys, so skip the check
+ // if the key is null
+ if (record.key() != null) {
+ final ValueAndTimestamp latestValueAndTimestamp = primaryKeyValueGetter.get(record.key());
+ if (latestValueAndTimestamp != null && latestValueAndTimestamp.timestamp() > record.timestamp()) {
+ LOG.info("Skipping out-of-order record from versioned table while performing table-table join.");
+ droppedRecordsSensor.record();
+ return;
+ }
+ }
+ }
+
final long[] currentHash = record.value().newValue == null ?
null :
Murmur3.hash128(valueSerializer.serialize(valueSerdeTopic, record.value().newValue));
@@ -107,37 +133,13 @@ public void process(final Record> record) {
if (record.value().oldValue != null) {
final KO oldForeignKey = foreignKeyExtractor.apply(record.value().oldValue);
if (oldForeignKey == null) {
- if (context().recordMetadata().isPresent()) {
- final RecordMetadata recordMetadata = context().recordMetadata().get();
- LOG.warn(
- "Skipping record due to null foreign key. "
- + "topic=[{}] partition=[{}] offset=[{}]",
- recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset()
- );
- } else {
- LOG.warn(
- "Skipping record due to null foreign key. Topic, partition, and offset not known."
- );
- }
- droppedRecordsSensor.record();
+ logSkippedRecordDueToNullForeignKey();
return;
}
if (record.value().newValue != null) {
final KO newForeignKey = foreignKeyExtractor.apply(record.value().newValue);
if (newForeignKey == null) {
- if (context().recordMetadata().isPresent()) {
- final RecordMetadata recordMetadata = context().recordMetadata().get();
- LOG.warn(
- "Skipping record due to null foreign key. "
- + "topic=[{}] partition=[{}] offset=[{}]",
- recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset()
- );
- } else {
- LOG.warn(
- "Skipping record due to null foreign key. Topic, partition, and offset not known."
- );
- }
- droppedRecordsSensor.record();
+ logSkippedRecordDueToNullForeignKey();
return;
}
@@ -193,19 +195,7 @@ public void process(final Record> record) {
}
final KO newForeignKey = foreignKeyExtractor.apply(record.value().newValue);
if (newForeignKey == null) {
- if (context().recordMetadata().isPresent()) {
- final RecordMetadata recordMetadata = context().recordMetadata().get();
- LOG.warn(
- "Skipping record due to null foreign key. "
- + "topic=[{}] partition=[{}] offset=[{}]",
- recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset()
- );
- } else {
- LOG.warn(
- "Skipping record due to null foreign key. Topic, partition, and offset not known."
- );
- }
- droppedRecordsSensor.record();
+ logSkippedRecordDueToNullForeignKey();
} else {
context().forward(
record.withKey(newForeignKey)
@@ -217,5 +207,26 @@ public void process(final Record> record) {
}
}
}
+
+ @Override
+ public void close() {
+ primaryKeyValueGetter.close();
+ }
+
+ private void logSkippedRecordDueToNullForeignKey() {
+ if (context().recordMetadata().isPresent()) {
+ final RecordMetadata recordMetadata = context().recordMetadata().get();
+ LOG.warn(
+ "Skipping record due to null foreign key. "
+ + "topic=[{}] partition=[{}] offset=[{}]",
+ recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset()
+ );
+ } else {
+ LOG.warn(
+ "Skipping record due to null foreign key. Topic, partition, and offset not known."
+ );
+ }
+ droppedRecordsSensor.record();
+ }
}
}
diff --git a/streams/src/test/java/org/apache/kafka/streams/integration/KTableKTableForeignKeyJoinIntegrationTest.java b/streams/src/test/java/org/apache/kafka/streams/integration/KTableKTableForeignKeyJoinIntegrationTest.java
index 931aaf8e53e65..04b124a726bfd 100644
--- a/streams/src/test/java/org/apache/kafka/streams/integration/KTableKTableForeignKeyJoinIntegrationTest.java
+++ b/streams/src/test/java/org/apache/kafka/streams/integration/KTableKTableForeignKeyJoinIntegrationTest.java
@@ -16,10 +16,12 @@
*/
package org.apache.kafka.streams.integration;
+import java.time.Duration;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.common.utils.Bytes;
+import org.apache.kafka.common.utils.MockTime;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.TestInputTopic;
@@ -66,25 +68,45 @@
public class KTableKTableForeignKeyJoinIntegrationTest {
@Rule
public Timeout globalTimeout = Timeout.seconds(600);
- private static final String LEFT_TABLE = "left_table";
- private static final String RIGHT_TABLE = "right_table";
- private static final String OUTPUT = "output-topic";
+ protected static final String LEFT_TABLE = "left_table";
+ protected static final String RIGHT_TABLE = "right_table";
+ protected static final String OUTPUT = "output-topic";
private static final String REJOIN_OUTPUT = "rejoin-output-topic";
- private final boolean leftJoin;
- private final boolean materialized;
+
+ private final MockTime time = new MockTime();
+
+ protected final boolean leftJoin;
+ protected final boolean materialized;
private final String optimization;
- private final boolean rejoin;
+ protected final boolean rejoin;
+ protected final boolean leftVersioned;
+ protected final boolean rightVersioned;
- private Properties streamsConfig;
+ protected Properties streamsConfig;
+ protected long baseTimestamp;
public KTableKTableForeignKeyJoinIntegrationTest(final boolean leftJoin,
final String optimization,
final boolean materialized,
final boolean rejoin) {
+ // versioning is disabled for these tests, even though the code supports building a
+ // topology with versioned tables, since KTableKTableForeignKeyVersionedJoinIntegrationTest
+ // extends this test class.
+ this(leftJoin, optimization, materialized, rejoin, false, false);
+ }
+
+ protected KTableKTableForeignKeyJoinIntegrationTest(final boolean leftJoin,
+ final String optimization,
+ final boolean materialized,
+ final boolean rejoin,
+ final boolean leftVersioned,
+ final boolean rightVersioned) {
this.rejoin = rejoin;
this.leftJoin = leftJoin;
this.materialized = materialized;
this.optimization = optimization;
+ this.leftVersioned = leftVersioned;
+ this.rightVersioned = rightVersioned;
}
@Rule
@@ -96,6 +118,7 @@ public void before() {
mkEntry(StreamsConfig.STATE_DIR_CONFIG, TestUtils.tempDirectory().getPath()),
mkEntry(StreamsConfig.TOPOLOGY_OPTIMIZATION_CONFIG, optimization)
));
+ baseTimestamp = time.milliseconds();
}
@Parameterized.Parameters(name = "leftJoin={0}, optimization={1}, materialized={2}, rejoin={3}")
@@ -105,7 +128,7 @@ public static Collection