This is an automated email from the ASF dual-hosted git repository.
mjsax pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new 34e04e35e05 KAFKA-20412: Fix prefixScan for KV-Store with headers
(#21971)
34e04e35e05 is described below
commit 34e04e35e057b22ec6b43d3ffbc73c037b7ce093
Author: Uladzislau Blok <[email protected]>
AuthorDate: Sun Jul 26 03:24:25 2026 +0200
KAFKA-20412: Fix prefixScan for KV-Store with headers (#21971)
Part of KIP-1271.
This PR closes a gap in headers passing. For `prefixScan`, we are not
yet correctly passing
Headers into the serdes.
Approach is similar to
`MeteredTimestampedKeyValueStoreWithHeaders#delete(key)`
Reviewers: Alieh Saeedi <[email protected]>, Matthias J. Sax
<[email protected]>
---
.../state/internals/CachingKeyValueStore.java | 1 +
.../state/internals/InMemoryKeyValueStore.java | 1 +
.../state/internals/MemoryNavigableLRUCache.java | 2 +-
.../state/internals/MeteredKeyValueStore.java | 6 ++-
...MeteredTimestampedKeyValueStoreWithHeaders.java | 6 ++-
.../state/internals/MeteredKeyValueStoreTest.java | 41 ++++++++++----
...redTimestampedKeyValueStoreWithHeadersTest.java | 62 ++++++++++++++++++++++
7 files changed, 105 insertions(+), 14 deletions(-)
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/CachingKeyValueStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/CachingKeyValueStore.java
index b6d85590091..1a1edf71692 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/CachingKeyValueStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/CachingKeyValueStore.java
@@ -438,6 +438,7 @@ public class CachingKeyValueStore
final ReadOnlyKeyValueStore<Bytes, byte[]> underlying) {
validateStoreOpen();
final KeyValueIterator<Bytes, byte[]> storeIterator =
underlying.prefixScan(prefix, prefixKeySerializer);
+ // headers aren't needed because the prefix already arrives serialized
final Bytes from = Bytes.wrap(prefixKeySerializer.serialize(null,
prefix));
final Bytes to = ByteUtils.increment(from);
final ThreadCache.MemoryLRUCacheBytesIterator cacheIterator =
internalContext.cache().range(cacheName, from, to, false);
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemoryKeyValueStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemoryKeyValueStore.java
index fcf570a101a..793d5b4cf2a 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemoryKeyValueStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemoryKeyValueStore.java
@@ -238,6 +238,7 @@ public class InMemoryKeyValueStore implements
KeyValueStore<Bytes, byte[]> {
private <PS extends Serializer<P>, P> KeyValueIterator<Bytes, byte[]>
prefixScan(final P prefix, final PS prefixKeySerializer,
final IsolationLevel isolationLevel) {
+ // headers aren't needed because the prefix already arrives serialized
final Bytes from = Bytes.wrap(prefixKeySerializer.serialize(null,
prefix));
final Bytes to = ByteUtils.increment(from);
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MemoryNavigableLRUCache.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MemoryNavigableLRUCache.java
index 0406f217aec..83506bc1e14 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MemoryNavigableLRUCache.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MemoryNavigableLRUCache.java
@@ -88,7 +88,7 @@ public class MemoryNavigableLRUCache extends MemoryLRUCache {
@Override
public <PS extends Serializer<P>, P> KeyValueIterator<Bytes, byte[]>
prefixScan(final P prefix, final PS prefixKeySerializer) {
-
+ // headers aren't needed because the prefix already arrives serialized
final Bytes from = Bytes.wrap(prefixKeySerializer.serialize(null,
prefix));
final Bytes to = ByteUtils.increment(from);
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
index ca8f12ac743..68c3795345f 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
@@ -19,6 +19,7 @@ package org.apache.kafka.streams.state.internals;
import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.metrics.Sensor;
+import org.apache.kafka.common.serialization.ByteArraySerializer;
import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.serialization.Serializer;
import org.apache.kafka.common.utils.Bytes;
@@ -79,6 +80,8 @@ public class MeteredKeyValueStore<K, V>
extends WrappedStateStore<KeyValueStore<Bytes, byte[]>, K, V>
implements KeyValueStore<K, V>, MeteredStateStore {
+ private static final Serializer<byte[]> BYTE_ARRAY_SERIALIZER = new
ByteArraySerializer();
+
final Serde<K> keySerde;
final Serde<V> valueSerde;
StateSerdes<K, V> serdes;
@@ -408,7 +411,8 @@ public class MeteredKeyValueStore<K, V>
private <PS extends Serializer<P>, P> KeyValueIterator<K, V>
prefixScanInternal(
final ReadOnlyKeyValueStore<Bytes, byte[]> store, final P prefix,
final PS prefixKeySerializer
) {
- return meteredKeyValueIterator(store.prefixScan(prefix,
prefixKeySerializer), prefixScanSensor);
+ final byte[] keyBytes = prefixKeySerializer.serialize(null,
internalContext.headers(), prefix);
+ return meteredKeyValueIterator(store.prefixScan(keyBytes,
BYTE_ARRAY_SERIALIZER), prefixScanSensor);
}
@Override
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeaders.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeaders.java
index 3d10e5569cc..4aa0b63982d 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeaders.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeaders.java
@@ -20,6 +20,7 @@ import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.metrics.Sensor;
+import org.apache.kafka.common.serialization.ByteArraySerializer;
import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.serialization.Serializer;
import org.apache.kafka.common.utils.Bytes;
@@ -79,6 +80,8 @@ public class MeteredTimestampedKeyValueStoreWithHeaders<K, V>
extends MeteredKeyValueStore<K, ValueTimestampHeaders<V>>
implements TimestampedKeyValueStoreWithHeaders<K, V> {
+ private static final Serializer<byte[]> BYTE_ARRAY_SERIALIZER = new
ByteArraySerializer();
+
MeteredTimestampedKeyValueStoreWithHeaders(
final KeyValueStore<Bytes, byte[]> inner,
final String metricScope,
@@ -592,8 +595,9 @@ public class MeteredTimestampedKeyValueStoreWithHeaders<K,
V>
private <PS extends Serializer<P>, P> KeyValueIterator<K,
ValueTimestampHeaders<V>> prefixScanInternal(
final ReadOnlyKeyValueStore<Bytes, byte[]> store, final P prefix,
final PS prefixKeySerializer
) {
+ final byte[] keyBytes = prefixKeySerializer.serialize(null,
internalContext.headers(), prefix);
return new MeteredTimestampedKeyValueStoreWithHeadersIterator(
- store.prefixScan(prefix, prefixKeySerializer), prefixScanSensor
+ store.prefixScan(keyBytes, BYTE_ARRAY_SERIALIZER), prefixScanSensor
);
}
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStoreTest.java
index d5d65e56be5..94911f5ece6 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStoreTest.java
@@ -27,6 +27,7 @@ import org.apache.kafka.common.metrics.KafkaMetricsContext;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.metrics.MetricsContext;
import org.apache.kafka.common.metrics.Sensor;
+import org.apache.kafka.common.serialization.ByteArraySerializer;
import org.apache.kafka.common.serialization.Deserializer;
import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.serialization.Serdes;
@@ -521,20 +522,26 @@ public class MeteredKeyValueStoreTest {
assertThrows(NullPointerException.class, () ->
metered.reverseRange("from", null));
}
+ @SuppressWarnings("unchecked")
@Test
public void shouldGetRecordsWithPrefixKey() {
setUp();
- final StringSerializer stringSerializer = new StringSerializer();
- when(inner.prefixScan(KEY, stringSerializer))
+ final Serializer<String> mockSerializer = mock(Serializer.class);
+ final Headers headers = new RecordHeaders();
+ when(context.headers()).thenReturn(headers);
+ when(mockSerializer.serialize(null, headers,
KEY)).thenReturn(KEY.getBytes(StandardCharsets.UTF_8));
+
+ when(inner.prefixScan(eq(KEY.getBytes(StandardCharsets.UTF_8)),
any(ByteArraySerializer.class)))
.thenReturn(new
KeyValueIteratorStub<>(Collections.singletonList(BYTE_KEY_VALUE_PAIR).iterator()));
init();
- final KeyValueIterator<String, String> iterator =
metered.prefixScan(KEY, stringSerializer);
+ final KeyValueIterator<String, String> iterator =
metered.prefixScan(KEY, mockSerializer);
assertThat(iterator.next().value, equalTo(VALUE));
iterator.close();
final KafkaMetric metric = metrics.metric(new
MetricName("prefix-scan-rate", STORE_LEVEL_GROUP, "", tags));
assertTrue((Double) metric.metricValue() > 0);
+ verify(mockSerializer).serialize(null, headers, KEY);
}
@Test
@@ -548,12 +555,17 @@ public class MeteredKeyValueStoreTest {
assertThat((Long) numKeysMetric.metricValue(), equalTo(-1L));
}
- @SuppressWarnings("unused")
+ @SuppressWarnings({"unused", "unchecked"})
@Test
public void shouldTrackOpenIteratorsMetric() {
setUp();
- final StringSerializer stringSerializer = new StringSerializer();
- when(inner.prefixScan(KEY,
stringSerializer)).thenReturn(KeyValueIterators.emptyIterator());
+ final Serializer<String> mockSerializer = mock(Serializer.class);
+ final Headers headers = new RecordHeaders();
+ when(context.headers()).thenReturn(headers);
+ when(mockSerializer.serialize(null, headers,
KEY)).thenReturn(KEY.getBytes(StandardCharsets.UTF_8));
+
+ when(inner.prefixScan(eq(KEY.getBytes(StandardCharsets.UTF_8)),
any(ByteArraySerializer.class)))
+ .thenReturn(KeyValueIterators.emptyIterator());
init();
final KafkaMetric openIteratorsMetric = metric("num-open-iterators");
@@ -561,11 +573,12 @@ public class MeteredKeyValueStoreTest {
assertThat((Long) openIteratorsMetric.metricValue(), equalTo(0L));
- try (final KeyValueIterator<String, String> unused =
metered.prefixScan(KEY, stringSerializer)) {
+ try (final KeyValueIterator<String, String> unused =
metered.prefixScan(KEY, mockSerializer)) {
assertThat((Long) openIteratorsMetric.metricValue(), equalTo(1L));
}
assertThat((Long) openIteratorsMetric.metricValue(), equalTo(0L));
+ verify(mockSerializer).serialize(null, headers, KEY);
}
@SuppressWarnings("unused")
@@ -734,19 +747,25 @@ public class MeteredKeyValueStoreTest {
public void
shouldReadOnlyViewPrefixScanApplySerdesAndRecordPrefixScanMetric() {
setUp();
final ReadOnlyKeyValueStore<Bytes, byte[]> innerView =
mock(ReadOnlyKeyValueStore.class);
- final StringSerializer stringSerializer = new StringSerializer();
+ final Serializer<String> mockSerializer = mock(Serializer.class);
+ final Headers headers = new RecordHeaders();
+ when(context.headers()).thenReturn(headers);
+ when(mockSerializer.serialize(null, headers,
KEY)).thenReturn(KEY.getBytes(StandardCharsets.UTF_8));
+
when(inner.readOnly(IsolationLevel.READ_UNCOMMITTED)).thenReturn(innerView);
- when(innerView.prefixScan(KEY, stringSerializer))
+ when(innerView.prefixScan(eq(KEY.getBytes(StandardCharsets.UTF_8)),
any(ByteArraySerializer.class)))
.thenReturn(new
KeyValueIteratorStub<>(Collections.singletonList(BYTE_KEY_VALUE_PAIR).iterator()));
init();
final ReadOnlyKeyValueStore<String, String> view =
metered.readOnly(IsolationLevel.READ_UNCOMMITTED);
- try (final KeyValueIterator<String, String> it = view.prefixScan(KEY,
stringSerializer)) {
+ try (final KeyValueIterator<String, String> it = view.prefixScan(KEY,
mockSerializer)) {
assertThat(it.next().value, equalTo(VALUE));
assertFalse(it.hasNext());
}
- assertTrue((Double) metrics.metric(new MetricName("prefix-scan-rate",
STORE_LEVEL_GROUP, "", tags)).metricValue() > 0);
+ final KafkaMetric metric = metric("prefix-scan-rate");
+ assertTrue((Double) metric.metricValue() > 0);
+ verify(mockSerializer).serialize(null, headers, KEY);
}
@SuppressWarnings("unchecked")
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeadersTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeadersTest.java
index 7a45afae1e8..14087a855ea 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeadersTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/MeteredTimestampedKeyValueStoreWithHeadersTest.java
@@ -15,7 +15,10 @@
* limitations under the License.
*/
package org.apache.kafka.streams.state.internals;
+
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.MetricName;
+import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.metrics.JmxReporter;
import org.apache.kafka.common.metrics.KafkaMetric;
@@ -29,6 +32,7 @@ import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.serialization.Serializer;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.common.utils.MockTime;
+import org.apache.kafka.common.utils.internals.LogContext;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.errors.StreamsException;
@@ -63,6 +67,7 @@ import org.mockito.quality.Strictness;
import java.io.Closeable;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -81,6 +86,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -868,4 +874,60 @@ public class
MeteredTimestampedKeyValueStoreWithHeadersTest {
// The critical verification: key deserializer must have been called
with HEADERS (not empty headers)
verify(keyDeserializer).deserialize(any(), eq(HEADERS),
eq(KEY.getBytes()));
}
+
+ @Test
+ public void shouldReturnRightEntriesForHeaderDependentSerdeInPrefixScan() {
+ setUp();
+ reset(inner);
+ final ThreadCache cache = new ThreadCache(new LogContext("testCache"),
1024L, context.metrics());
+ when(context.cache()).thenReturn(cache);
+ when(context.headers()).thenReturn(HEADERS);
+
+ final InMemoryKeyValueStore inMemoryStore = new
InMemoryKeyValueStore(STORE_NAME);
+
+ final Serializer<String> headerDependentSerializer = new
Serializer<>() {
+ @Override
+ public byte[] serialize(final String topic, final String data) {
+ return serialize(topic, null, data);
+ }
+
+ @Override
+ public byte[] serialize(final String topic, final Headers headers,
final String data) {
+ final byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
+ final byte[] result = new byte[bytes.length + 1];
+ result[0] = headers == null ? (byte) 1 : (byte) 2;
+ System.arraycopy(bytes, 0, result, 1, bytes.length);
+ return result;
+ }
+ };
+
+ final MeteredTimestampedKeyValueStoreWithHeaders<String, String> store
= new MeteredTimestampedKeyValueStoreWithHeaders<>(
+ new CachingKeyValueStoreWithHeaders(inMemoryStore),
+ "scope",
+ new MockTime(),
+ Serdes.serdeFrom(headerDependentSerializer,
Serdes.String().deserializer()),
+ new ValueTimestampHeadersSerde<>(Serdes.String())
+ );
+
+ store.init(context, store);
+
+ store.put("key1", ValueTimestampHeaders.make("value1", 100L, HEADERS));
+ store.put("key2", ValueTimestampHeaders.make("value2", 100L, HEADERS));
+
+ // 1. Test default prefixScan
+ try (final KeyValueIterator<String, ValueTimestampHeaders<String>>
iterator = store.prefixScan("key", headerDependentSerializer)) {
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.next().key.endsWith("key1"));
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.next().key.endsWith("key2"));
+ }
+
+ // 2. Test readOnly prefixScan
+ try (final KeyValueIterator<String, ValueTimestampHeaders<String>>
iterator = store.readOnly(IsolationLevel.READ_UNCOMMITTED).prefixScan("key",
headerDependentSerializer)) {
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.next().key.endsWith("key1"));
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.next().key.endsWith("key2"));
+ }
+ }
}