mjsax commented on code in PR #23144:
URL: https://github.com/apache/kafka/pull/23144#discussion_r3771162030
##########
streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java:
##########
@@ -1074,6 +1081,18 @@ interface DBAccessor {
void reset();
void close();
+ /**
+ * Applies a batch of writes through {@code cfAccessor}, which owns
the column-family layout.
+ * The default applies the entries one at a time; accessors that can
write them as a single
+ * batch — or that must stage them instead of writing them — override
this.
+ */
+ default void putAll(final ColumnFamilyAccessor cfAccessor,
Review Comment:
Just curious about having a `default` impl -- it seems both
`DirectDBAccessor` and `TransactionalDBAccessor` actually overwrite it and
don't use the default. So why do we have it?
Also wondering about the existing `default` impl of `readOnly()` below,
which does ignore its `isolationLevel` parameter, so wondering if this default
impl could even be correct to begin with? (Even if it's overwritten only by
`TransactionalDBAccessor`. Just curious to learn more, as I am not familiar
with the details of KIP-892 implemenation.
##########
streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBStoreTest.java:
##########
@@ -1664,6 +1664,125 @@ public void
offsetColumnFamilyWritesShouldNotLeakIntoDataIteration() {
}
}
+ @Test
+ public void putAllShouldStageWritesUntilCommitWhenTransactional() {
+ rocksDBStore.close();
+ final InternalMockProcessorContext<?, ?> eosContext =
getTransactionalEOSProcessorContext(dir);
+ rocksDBStore = getRocksDBStore();
+ rocksDBStore.init(eosContext, rocksDBStore);
+
+ // An empty RocksDB WriteBatch already reports a fixed header size, so
the baseline is non-zero.
+ final long emptyBufferBytes =
rocksDBStore.approximateNumUncommittedBytes();
+
+ final Bytes k1 = new Bytes(stringSerializer.serialize(null, "k1"));
+ final Bytes k2 = new Bytes(stringSerializer.serialize(null, "k2"));
+ rocksDBStore.putAll(List.of(
+ KeyValue.pair(k1, stringSerializer.serialize(null, "v1")),
+ KeyValue.pair(k2, stringSerializer.serialize(null, "v2"))));
+
+ final ReadOnlyKeyValueStore<Bytes, byte[]> uncommitted =
rocksDBStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final ReadOnlyKeyValueStore<Bytes, byte[]> committed =
rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED);
+
+ // the batch is staged rather than written: it counts towards the
uncommitted byte total ...
+ assertTrue(rocksDBStore.approximateNumUncommittedBytes() >
emptyBufferBytes);
+ // ... is visible to the owner and at READ_UNCOMMITTED ...
+ assertEquals("v1", stringDeserializer.deserialize(null,
rocksDBStore.get(k1)));
+ assertEquals("v2", stringDeserializer.deserialize(null,
uncommitted.get(k2)));
+ // ... and stays hidden at READ_COMMITTED until the store commits.
+ assertNull(committed.get(k1));
+ assertNull(committed.get(k2));
+
+ rocksDBStore.commit(Map.of());
+
+ assertEquals("v1", stringDeserializer.deserialize(null,
committed.get(k1)));
+ assertEquals("v2", stringDeserializer.deserialize(null,
committed.get(k2)));
+ }
+
+ @Test
+ public void putAllShouldBeDiscardedOnRollbackWhenTransactional() {
+ rocksDBStore.close();
+ final InternalMockProcessorContext<?, ?> eosContext =
getTransactionalEOSProcessorContext(dir);
+ rocksDBStore = getRocksDBStore();
+ rocksDBStore.init(eosContext, rocksDBStore);
+
+ final Bytes k1 = new Bytes(stringSerializer.serialize(null, "k1"));
+ final Bytes k2 = new Bytes(stringSerializer.serialize(null, "k2"));
+ rocksDBStore.put(k1, stringSerializer.serialize(null, "committed"));
+ rocksDBStore.commit(Map.of());
+
+ rocksDBStore.putAll(List.of(
+ KeyValue.pair(k1, stringSerializer.serialize(null, "rolled-back")),
+ KeyValue.pair(k2, stringSerializer.serialize(null,
"rolled-back"))));
+ rocksDBStore.dbAccessor.rollbackStagedWrites();
+
+ // an aborted batch must leave nothing behind: the committed value is
intact and the new key is gone
+ assertEquals("committed", stringDeserializer.deserialize(null,
rocksDBStore.get(k1)));
+ assertNull(rocksDBStore.get(k2));
+ assertEquals("committed", stringDeserializer.deserialize(null,
+ rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED).get(k1)));
+
assertNull(rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED).get(k2));
Review Comment:
Not sure why we test READ_COMMITTED here? For this isolation level, even if
the write is pending, it's not visible, right? Isn't the more interesting case
READ_UNCOMMITTED, to verify that the buffer was dropped?
##########
streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java:
##########
@@ -512,10 +512,17 @@ public synchronized byte[] putIfAbsent(final Bytes key,
@Override
public void putAll(final List<KeyValue<Bytes, byte[]>> entries) {
+ Objects.requireNonNull(entries, "entries cannot be null");
+ // Validate up front so a null key rejects the whole batch. An
accessor may apply the entries
+ // one at a time, and failing part-way through would otherwise leave
the batch half-applied.
+ for (final KeyValue<Bytes, byte[]> entry : entries) {
+ Objects.requireNonNull(entry, "entry cannot be null");
+ Objects.requireNonNull(entry.key, "key cannot be null");
+ }
+ validateStoreOpen();
Review Comment:
Why do we need to do this? We didn't have this in the existing code either.
##########
streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java:
##########
@@ -1251,6 +1280,23 @@ public void deleteRange(final ColumnFamilyHandle
columnFamily, final byte[] from
buffer.stageDeleteRange(columnFamily, Bytes.wrap(from),
Bytes.wrap(to));
}
+ @Override
+ public void putAll(final ColumnFamilyAccessor cfAccessor,
+ final List<KeyValue<Bytes, byte[]>> entries) {
+ // Batch writes must be staged like single-key puts, or they would
land in the store
+ // uncommitted: visible at READ_COMMITTED, surviving a rollback,
and uncounted by
Review Comment:
> visible at READ_COMMITTED
This is confusing me? If we stage something, should it not be invisible
until committed for READ_COMMITTED mode?
> surviving a rollback
Similar. If we have an error and roll-back, should we not throw away all
state data?
> uncounted by approximateNumUncommittedBytes()
Same. Seems off if stage data is not included in "uncommitted bytes"?
##########
streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBStoreTest.java:
##########
@@ -1664,6 +1664,125 @@ public void
offsetColumnFamilyWritesShouldNotLeakIntoDataIteration() {
}
}
+ @Test
+ public void putAllShouldStageWritesUntilCommitWhenTransactional() {
+ rocksDBStore.close();
+ final InternalMockProcessorContext<?, ?> eosContext =
getTransactionalEOSProcessorContext(dir);
+ rocksDBStore = getRocksDBStore();
+ rocksDBStore.init(eosContext, rocksDBStore);
+
+ // An empty RocksDB WriteBatch already reports a fixed header size, so
the baseline is non-zero.
+ final long emptyBufferBytes =
rocksDBStore.approximateNumUncommittedBytes();
+
+ final Bytes k1 = new Bytes(stringSerializer.serialize(null, "k1"));
+ final Bytes k2 = new Bytes(stringSerializer.serialize(null, "k2"));
+ rocksDBStore.putAll(List.of(
+ KeyValue.pair(k1, stringSerializer.serialize(null, "v1")),
+ KeyValue.pair(k2, stringSerializer.serialize(null, "v2"))));
+
+ final ReadOnlyKeyValueStore<Bytes, byte[]> uncommitted =
rocksDBStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final ReadOnlyKeyValueStore<Bytes, byte[]> committed =
rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED);
+
+ // the batch is staged rather than written: it counts towards the
uncommitted byte total ...
+ assertTrue(rocksDBStore.approximateNumUncommittedBytes() >
emptyBufferBytes);
+ // ... is visible to the owner and at READ_UNCOMMITTED ...
+ assertEquals("v1", stringDeserializer.deserialize(null,
rocksDBStore.get(k1)));
+ assertEquals("v2", stringDeserializer.deserialize(null,
uncommitted.get(k2)));
+ // ... and stays hidden at READ_COMMITTED until the store commits.
+ assertNull(committed.get(k1));
+ assertNull(committed.get(k2));
+
+ rocksDBStore.commit(Map.of());
+
+ assertEquals("v1", stringDeserializer.deserialize(null,
committed.get(k1)));
+ assertEquals("v2", stringDeserializer.deserialize(null,
committed.get(k2)));
+ }
+
+ @Test
+ public void putAllShouldBeDiscardedOnRollbackWhenTransactional() {
+ rocksDBStore.close();
+ final InternalMockProcessorContext<?, ?> eosContext =
getTransactionalEOSProcessorContext(dir);
+ rocksDBStore = getRocksDBStore();
+ rocksDBStore.init(eosContext, rocksDBStore);
+
+ final Bytes k1 = new Bytes(stringSerializer.serialize(null, "k1"));
+ final Bytes k2 = new Bytes(stringSerializer.serialize(null, "k2"));
+ rocksDBStore.put(k1, stringSerializer.serialize(null, "committed"));
+ rocksDBStore.commit(Map.of());
+
+ rocksDBStore.putAll(List.of(
+ KeyValue.pair(k1, stringSerializer.serialize(null, "rolled-back")),
+ KeyValue.pair(k2, stringSerializer.serialize(null,
"rolled-back"))));
+ rocksDBStore.dbAccessor.rollbackStagedWrites();
+
+ // an aborted batch must leave nothing behind: the committed value is
intact and the new key is gone
+ assertEquals("committed", stringDeserializer.deserialize(null,
rocksDBStore.get(k1)));
+ assertNull(rocksDBStore.get(k2));
+ assertEquals("committed", stringDeserializer.deserialize(null,
+ rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED).get(k1)));
+
assertNull(rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED).get(k2));
+ }
+
+ @Test
+ public void putAllShouldStageTombstonesWhenTransactional() {
+ rocksDBStore.close();
+ final InternalMockProcessorContext<?, ?> eosContext =
getTransactionalEOSProcessorContext(dir);
+ rocksDBStore = getRocksDBStore();
+ rocksDBStore.init(eosContext, rocksDBStore);
+
+ final Bytes k1 = new Bytes(stringSerializer.serialize(null, "k1"));
+ final Bytes k2 = new Bytes(stringSerializer.serialize(null, "k2"));
+ rocksDBStore.put(k1, stringSerializer.serialize(null, "v1"));
+ rocksDBStore.put(k2, stringSerializer.serialize(null, "v2"));
+ rocksDBStore.commit(Map.of());
+
+ // a null value in the batch is a delete, and must be staged like any
other write
+ rocksDBStore.putAll(Arrays.asList(
+ KeyValue.pair(k1, null),
+ KeyValue.pair(k2, stringSerializer.serialize(null,
"v2-updated"))));
+
+ final ReadOnlyKeyValueStore<Bytes, byte[]> committed =
rocksDBStore.readOnly(IsolationLevel.READ_COMMITTED);
+ assertNull(rocksDBStore.get(k1));
+ assertEquals("v1", stringDeserializer.deserialize(null,
committed.get(k1)));
+ assertEquals("v2-updated", stringDeserializer.deserialize(null,
+ rocksDBStore.readOnly(IsolationLevel.READ_UNCOMMITTED).get(k2)));
Review Comment:
Should we also test uncommitted k1 (or would it be redundant to
`assertNull(rocksDBStore.get(k1));` ?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]