Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ <K, V> 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<TopicPartition, Long> offsets();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -279,7 +280,7 @@ public void close() {

@Override
public Map<TopicPartition, Long> offsets() {
return offsets;
return Collections.unmodifiableMap(offsets);

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.

Would it be simpler to hand out a deep copy of the map directly?

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.

That would be totally fine for the current usage pattern, where we have a single query that ends up copying the map anyway. However, if we ever ended up with other queries then those queries would pay the cost of the copy whether they need it or not. It would be a bit surprising that a copy is happening without looking at the implementation. My bias would be towards defensive coding without surprise performance impact.

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.

Fair enough.

}

// for testing only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,8 @@ void commit(final boolean startNewTransaction) {

@Override
protected Map<TopicPartition, Long> activeTaskCheckpointableOffsets() {
final Map<TopicPartition, Long> checkpointableOffsets = recordCollector.offsets();
final Map<TopicPartition, Long> checkpointableOffsets =
new HashMap<>(recordCollector.offsets());
for (final Map.Entry<TopicPartition, Long> entry : consumedOffsets.entrySet()) {
checkpointableOffsets.putIfAbsent(entry.getKey(), entry.getValue());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<TopicPartition, Long> offsets = collector.offsets();

assertThat(offsets.get(topicPartition), equalTo(2L));
assertThrows(UnsupportedOperationException.class, () -> offsets.put(new TopicPartition(topic, 0), 50L));

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.

nit: I would remove this line

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.

I would argue for keeping this because the change impacts the external behavior of the class. We're making a strong statement here: you will get an exception if you try to modify the contents of the returned map, you must copy this map if you want to make changes. This also distinguishes from the alternative approach we could have used in which we proactively copy the map for the user and where the user could have made a change to the map while still not impacting the underlying map. Given that this is externally facing and there is doc a couple levels up, I will fix that up.

Happy to discuss further if you strongly disagree.

assertThat(collector.offsets().get(topicPartition), equalTo(2L));
}

@SuppressWarnings("unchecked")
@Test(expected = StreamsException.class)
public void shouldThrowStreamsExceptionOnAnyExceptionButProducerFencedException() {
Expand Down