diff --git a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorContextImpl.java b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorContextImpl.java index c1c3a6064794e..2d93a81cf1cb9 100644 --- a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorContextImpl.java +++ b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorContextImpl.java @@ -146,12 +146,15 @@ public void forward(final K key, public void forward(final K key, final V value, final To to) { - toInternal.update(to); - if (toInternal.hasTimestamp()) { - recordContext.setTimestamp(toInternal.timestamp()); - } final ProcessorNode previousNode = currentNode(); + final long currentTimestamp = recordContext.timestamp; + try { + toInternal.update(to); + if (toInternal.hasTimestamp()) { + recordContext.setTimestamp(toInternal.timestamp()); + } + final String sendTo = toInternal.child(); if (sendTo == null) { final List> children = (List>) currentNode().children(); @@ -167,6 +170,7 @@ public void forward(final K key, forward(child, key, value); } } finally { + recordContext.timestamp = currentTimestamp; setCurrentNode(previousNode); } } diff --git a/streams/src/test/java/org/apache/kafka/streams/processor/internals/ProcessorTopologyTest.java b/streams/src/test/java/org/apache/kafka/streams/processor/internals/ProcessorTopologyTest.java index 6a7bd027bf8c6..f6736d2ff4a78 100644 --- a/streams/src/test/java/org/apache/kafka/streams/processor/internals/ProcessorTopologyTest.java +++ b/streams/src/test/java/org/apache/kafka/streams/processor/internals/ProcessorTopologyTest.java @@ -348,6 +348,32 @@ public void shouldConsiderModifiedTimeStamps() { assertNextOutputRecord(OUTPUT_TOPIC_1, "key3", "value3", partition, 40L); } + @Test + public void shouldConsiderModifiedTimeStampsForMultipleProcessors() { + final int partition = 10; + driver = new TopologyTestDriver(createMultiProcessorTimestampTopology(partition), props); + + driver.pipeInput(recordFactory.create(INPUT_TOPIC_1, "key1", "value1", 10L)); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key1", "value1", partition, 10L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key1", "value1", partition, 20L); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key1", "value1", partition, 15L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key1", "value1", partition, 20L); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key1", "value1", partition, 12L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key1", "value1", partition, 22L); + assertNoOutputRecord(OUTPUT_TOPIC_1); + assertNoOutputRecord(OUTPUT_TOPIC_2); + + driver.pipeInput(recordFactory.create(INPUT_TOPIC_1, "key2", "value2", 20L)); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key2", "value2", partition, 20L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key2", "value2", partition, 30L); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key2", "value2", partition, 25L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key2", "value2", partition, 30L); + assertNextOutputRecord(OUTPUT_TOPIC_1, "key2", "value2", partition, 22L); + assertNextOutputRecord(OUTPUT_TOPIC_2, "key2", "value2", partition, 32L); + assertNoOutputRecord(OUTPUT_TOPIC_1); + assertNoOutputRecord(OUTPUT_TOPIC_2); + } + @Test public void shouldConsiderHeaders() { final int partition = 10; @@ -489,6 +515,16 @@ private Topology createTimestampTopology(final int partition) { .addSink("sink", OUTPUT_TOPIC_1, constantPartitioner(partition), "processor"); } + private Topology createMultiProcessorTimestampTopology(final int partition) { + return topology + .addSource("source", STRING_DESERIALIZER, STRING_DESERIALIZER, INPUT_TOPIC_1) + .addProcessor("processor", define(new FanOutTimestampProcessor("child1", "child2")), "source") + .addProcessor("child1", define(new ForwardingProcessor()), "processor") + .addProcessor("child2", define(new TimestampProcessor()), "processor") + .addSink("sink1", OUTPUT_TOPIC_1, constantPartitioner(partition), "child1") + .addSink("sink2", OUTPUT_TOPIC_2, constantPartitioner(partition), "child2"); + } + private Topology createMultiplexingTopology() { return topology .addSource("source", STRING_DESERIALIZER, STRING_DESERIALIZER, INPUT_TOPIC_1) @@ -582,6 +618,25 @@ public void process(final String key, final String value) { } } + protected static class FanOutTimestampProcessor extends AbstractProcessor { + private final String firstChild; + private final String secondChild; + + FanOutTimestampProcessor(final String firstChild, + final String secondChild) { + this.firstChild = firstChild; + this.secondChild = secondChild; + } + + @Override + public void process(final String key, final String value) { + context().forward(key, value); + context().forward(key, value, To.child(firstChild).withTimestamp(context().timestamp() + 5)); + context().forward(key, value, To.child(secondChild)); + context().forward(key, value, To.all().withTimestamp(context().timestamp() + 2)); + } + } + protected static class AddHeaderProcessor extends AbstractProcessor { @Override public void process(final String key, final String value) {