diff --git a/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollector.java b/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollector.java index bbfb04941e3c5..b8b99a3f7b9ba 100644 --- a/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollector.java +++ b/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollector.java @@ -63,7 +63,7 @@ void send(final String topic, /** * The last acked offsets from the internal {@link Producer}. * - * @return the map from TopicPartition to offset + * @return an immutable map from TopicPartition to offset */ Map offsets(); diff --git a/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollectorImpl.java b/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollectorImpl.java index c6bcda6d4ecf0..8430ff1be917b 100644 --- a/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollectorImpl.java +++ b/streams/src/main/java/org/apache/kafka/streams/processor/internals/RecordCollectorImpl.java @@ -16,6 +16,7 @@ */ package org.apache.kafka.streams.processor.internals; +import java.util.Collections; import org.apache.kafka.clients.producer.Callback; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; @@ -279,7 +280,7 @@ public void close() { @Override public Map offsets() { - return offsets; + return Collections.unmodifiableMap(offsets); } // for testing only diff --git a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java index 836330f242617..d50d5c23cc6c9 100644 --- a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java +++ b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java @@ -486,7 +486,8 @@ void commit(final boolean startNewTransaction) { @Override protected Map activeTaskCheckpointableOffsets() { - final Map checkpointableOffsets = recordCollector.offsets(); + final Map checkpointableOffsets = + new HashMap<>(recordCollector.offsets()); for (final Map.Entry entry : consumedOffsets.entrySet()) { checkpointableOffsets.putIfAbsent(entry.getKey(), entry.getValue()); } diff --git a/streams/src/test/java/org/apache/kafka/streams/processor/internals/RecordCollectorTest.java b/streams/src/test/java/org/apache/kafka/streams/processor/internals/RecordCollectorTest.java index 47dd61b2a9057..f40f3c1871c41 100644 --- a/streams/src/test/java/org/apache/kafka/streams/processor/internals/RecordCollectorTest.java +++ b/streams/src/test/java/org/apache/kafka/streams/processor/internals/RecordCollectorTest.java @@ -50,7 +50,10 @@ import java.util.Map; import java.util.concurrent.Future; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -145,6 +148,31 @@ public void testStreamPartitioner() { assertEquals((Long) 0L, offsets.get(new TopicPartition("topic1", 2))); } + @Test + public void shouldNotAllowOffsetsToBeUpdatedExternally() { + final String topic = "topic1"; + final TopicPartition topicPartition = new TopicPartition(topic, 0); + + final RecordCollectorImpl collector = new RecordCollectorImpl( + "RecordCollectorTest-TestSpecificPartition", + new LogContext("RecordCollectorTest-TestSpecificPartition "), + new DefaultProductionExceptionHandler(), + new Metrics().sensor("skipped-records") + ); + collector.init(new MockProducer<>(cluster, true, new DefaultPartitioner(), byteArraySerializer, byteArraySerializer)); + + collector.send(topic, "999", "0", null, 0, null, stringSerializer, stringSerializer); + collector.send(topic, "999", "0", null, 0, null, stringSerializer, stringSerializer); + collector.send(topic, "999", "0", null, 0, null, stringSerializer, stringSerializer); + + final Map offsets = collector.offsets(); + + assertThat(offsets.get(topicPartition), equalTo(2L)); + assertThrows(UnsupportedOperationException.class, () -> offsets.put(new TopicPartition(topic, 0), 50L)); + + assertThat(collector.offsets().get(topicPartition), equalTo(2L)); + } + @SuppressWarnings("unchecked") @Test(expected = StreamsException.class) public void shouldThrowStreamsExceptionOnAnyExceptionButProducerFencedException() {