Add FlatDbStrategy (#5901)

* move FlatDbReader to FlatDbStrategy (including writes), add getNearestTo

Signed-off-by: garyschulte <garyschulte@gmail.com>
pull/5939/head
garyschulte 1 year ago committed by GitHub
parent 9d7ee2b52b
commit d81e1f3042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiSnapshotWorldStateKeyValueStorage.java
  2. 100
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorage.java
  3. 76
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/flat/FlatDbStrategy.java
  4. 8
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/flat/FullFlatDbStrategy.java
  5. 8
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/flat/PartialFlatDbStrategy.java
  6. 4
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/worldview/BonsaiWorldState.java
  7. 5
      ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java
  8. 2
      plugin-api/build.gradle
  9. 31
      plugin-api/src/main/java/org/hyperledger/besu/plugin/services/storage/SegmentedKeyValueStorage.java
  10. 15
      plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueSnapshot.java
  11. 14
      plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBColumnarKeyValueStorage.java
  12. 13
      plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshotTransaction.java
  13. 2
      services/kvstore/src/main/java/org/hyperledger/besu/services/kvstore/InMemoryKeyValueStorage.java
  14. 20
      services/kvstore/src/main/java/org/hyperledger/besu/services/kvstore/LayeredKeyValueStorage.java
  15. 27
      services/kvstore/src/main/java/org/hyperledger/besu/services/kvstore/SegmentedInMemoryKeyValueStorage.java
  16. 4
      services/kvstore/src/main/java/org/hyperledger/besu/services/kvstore/SegmentedKeyValueStorageAdapter.java
  17. 82
      services/kvstore/src/test/java/org/hyperledger/besu/services/kvstore/AbstractSegmentedKeyValueStorageTest.java
  18. 9
      services/kvstore/src/test/java/org/hyperledger/besu/services/kvstore/InMemoryKeyValueStorageTest.java
  19. 33
      services/kvstore/src/test/java/org/hyperledger/besu/services/kvstore/LayeredKeyValueStorageTest.java

@ -47,7 +47,7 @@ public class BonsaiSnapshotWorldStateKeyValueStorage extends BonsaiWorldStateKey
final ObservableMetricsSystem metricsSystem) { final ObservableMetricsSystem metricsSystem) {
super( super(
parentWorldStateStorage.flatDbMode, parentWorldStateStorage.flatDbMode,
parentWorldStateStorage.flatDbReaderStrategy, parentWorldStateStorage.flatDbStrategy,
segmentedWorldStateStorage, segmentedWorldStateStorage,
trieLogStorage, trieLogStorage,
metricsSystem); metricsSystem);
@ -77,7 +77,8 @@ public class BonsaiSnapshotWorldStateKeyValueStorage extends BonsaiWorldStateKey
public BonsaiUpdater updater() { public BonsaiUpdater updater() {
return new Updater( return new Updater(
((SnappedKeyValueStorage) composedWorldStateStorage).getSnapshotTransaction(), ((SnappedKeyValueStorage) composedWorldStateStorage).getSnapshotTransaction(),
trieLogStorage.startTransaction()); trieLogStorage.startTransaction(),
flatDbStrategy);
} }
@Override @Override

@ -21,9 +21,9 @@ import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIden
import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.StorageSlotKey; import org.hyperledger.besu.datatypes.StorageSlotKey;
import org.hyperledger.besu.ethereum.bonsai.storage.flat.FlatDbReaderStrategy; import org.hyperledger.besu.ethereum.bonsai.storage.flat.FlatDbStrategy;
import org.hyperledger.besu.ethereum.bonsai.storage.flat.FullFlatDbReaderStrategy; import org.hyperledger.besu.ethereum.bonsai.storage.flat.FullFlatDbStrategy;
import org.hyperledger.besu.ethereum.bonsai.storage.flat.PartialFlatDbReaderStrategy; import org.hyperledger.besu.ethereum.bonsai.storage.flat.PartialFlatDbStrategy;
import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.storage.StorageProvider;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier;
import org.hyperledger.besu.ethereum.trie.MerkleTrie; import org.hyperledger.besu.ethereum.trie.MerkleTrie;
@ -67,7 +67,7 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
public static final byte[] FLAT_DB_MODE = "flatDbStatus".getBytes(StandardCharsets.UTF_8); public static final byte[] FLAT_DB_MODE = "flatDbStatus".getBytes(StandardCharsets.UTF_8);
protected FlatDbMode flatDbMode; protected FlatDbMode flatDbMode;
protected FlatDbReaderStrategy flatDbReaderStrategy; protected FlatDbStrategy flatDbStrategy;
protected final SegmentedKeyValueStorage composedWorldStateStorage; protected final SegmentedKeyValueStorage composedWorldStateStorage;
protected final KeyValueStorage trieLogStorage; protected final KeyValueStorage trieLogStorage;
@ -94,33 +94,49 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
public BonsaiWorldStateKeyValueStorage( public BonsaiWorldStateKeyValueStorage(
final FlatDbMode flatDbMode, final FlatDbMode flatDbMode,
final FlatDbReaderStrategy flatDbReaderStrategy, final FlatDbStrategy flatDbStrategy,
final SegmentedKeyValueStorage composedWorldStateStorage, final SegmentedKeyValueStorage composedWorldStateStorage,
final KeyValueStorage trieLogStorage, final KeyValueStorage trieLogStorage,
final ObservableMetricsSystem metricsSystem) { final ObservableMetricsSystem metricsSystem) {
this.flatDbMode = flatDbMode; this.flatDbMode = flatDbMode;
this.flatDbReaderStrategy = flatDbReaderStrategy; this.flatDbStrategy = flatDbStrategy;
this.composedWorldStateStorage = composedWorldStateStorage; this.composedWorldStateStorage = composedWorldStateStorage;
this.trieLogStorage = trieLogStorage; this.trieLogStorage = trieLogStorage;
this.metricsSystem = metricsSystem; this.metricsSystem = metricsSystem;
} }
public void loadFlatDbStrategy() { private void loadFlatDbStrategy() {
this.flatDbMode = // derive our flatdb strategy from db or default:
var newFlatDbMode = deriveFlatDbStrategy();
// if flatDbMode is not loaded or has changed, reload flatDbStrategy
if (this.flatDbMode == null || !this.flatDbMode.equals(newFlatDbMode)) {
this.flatDbMode = newFlatDbMode;
if (flatDbMode == FlatDbMode.FULL) {
this.flatDbStrategy = new FullFlatDbStrategy(metricsSystem);
} else {
this.flatDbStrategy = new PartialFlatDbStrategy(metricsSystem);
}
}
}
public FlatDbMode deriveFlatDbStrategy() {
var flatDbMode =
FlatDbMode.fromVersion( FlatDbMode.fromVersion(
composedWorldStateStorage composedWorldStateStorage
.get(TRIE_BRANCH_STORAGE, FLAT_DB_MODE) .get(TRIE_BRANCH_STORAGE, FLAT_DB_MODE)
.map(Bytes::wrap) .map(Bytes::wrap)
.orElse( .orElse(FlatDbMode.PARTIAL.getVersion()));
FlatDbMode.PARTIAL
.getVersion())); // for backward compatibility we use partial as
// default
LOG.info("Bonsai flat db mode found {}", flatDbMode); LOG.info("Bonsai flat db mode found {}", flatDbMode);
if (flatDbMode == FlatDbMode.FULL) {
this.flatDbReaderStrategy = new FullFlatDbReaderStrategy(metricsSystem); return flatDbMode;
} else { }
this.flatDbReaderStrategy = new PartialFlatDbReaderStrategy(metricsSystem);
public FlatDbStrategy getFlatDbStrategy() {
if (flatDbStrategy == null) {
loadFlatDbStrategy();
} }
return flatDbStrategy;
} }
@Override @Override
@ -133,22 +149,18 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
return flatDbMode; return flatDbMode;
} }
public FlatDbReaderStrategy getFlatDbReaderStrategy() {
return flatDbReaderStrategy;
}
@Override @Override
public Optional<Bytes> getCode(final Bytes32 codeHash, final Hash accountHash) { public Optional<Bytes> getCode(final Bytes32 codeHash, final Hash accountHash) {
if (codeHash.equals(Hash.EMPTY)) { if (codeHash.equals(Hash.EMPTY)) {
return Optional.of(Bytes.EMPTY); return Optional.of(Bytes.EMPTY);
} else { } else {
return getFlatDbReaderStrategy().getCode(codeHash, accountHash, composedWorldStateStorage); return getFlatDbStrategy().getFlatCode(codeHash, accountHash, composedWorldStateStorage);
} }
} }
public Optional<Bytes> getAccount(final Hash accountHash) { public Optional<Bytes> getAccount(final Hash accountHash) {
return getFlatDbReaderStrategy() return getFlatDbStrategy()
.getAccount( .getFlatAccount(
this::getWorldStateRootHash, this::getWorldStateRootHash,
this::getAccountStateTrieNode, this::getAccountStateTrieNode,
accountHash, accountHash,
@ -226,8 +238,8 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
final Supplier<Optional<Hash>> storageRootSupplier, final Supplier<Optional<Hash>> storageRootSupplier,
final Hash accountHash, final Hash accountHash,
final StorageSlotKey storageSlotKey) { final StorageSlotKey storageSlotKey) {
return getFlatDbReaderStrategy() return getFlatDbStrategy()
.getStorageValueByStorageSlotKey( .getFlatStorageValueByStorageSlotKey(
this::getWorldStateRootHash, this::getWorldStateRootHash,
storageRootSupplier, storageRootSupplier,
(location, hash) -> getAccountStorageTrieNode(accountHash, location, hash), (location, hash) -> getAccountStorageTrieNode(accountHash, location, hash),
@ -239,14 +251,14 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
@Override @Override
public Map<Bytes32, Bytes> streamFlatAccounts( public Map<Bytes32, Bytes> streamFlatAccounts(
final Bytes startKeyHash, final Bytes32 endKeyHash, final long max) { final Bytes startKeyHash, final Bytes32 endKeyHash, final long max) {
return getFlatDbReaderStrategy() return getFlatDbStrategy()
.streamAccountFlatDatabase(composedWorldStateStorage, startKeyHash, endKeyHash, max); .streamAccountFlatDatabase(composedWorldStateStorage, startKeyHash, endKeyHash, max);
} }
@Override @Override
public Map<Bytes32, Bytes> streamFlatStorages( public Map<Bytes32, Bytes> streamFlatStorages(
final Hash accountHash, final Bytes startKeyHash, final Bytes32 endKeyHash, final long max) { final Hash accountHash, final Bytes startKeyHash, final Bytes32 endKeyHash, final long max) {
return getFlatDbReaderStrategy() return getFlatDbStrategy()
.streamStorageFlatDatabase( .streamStorageFlatDatabase(
composedWorldStateStorage, accountHash, startKeyHash, endKeyHash, max); composedWorldStateStorage, accountHash, startKeyHash, endKeyHash, max);
} }
@ -273,6 +285,7 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
public void upgradeToFullFlatDbMode() { public void upgradeToFullFlatDbMode() {
final SegmentedKeyValueStorageTransaction transaction = final SegmentedKeyValueStorageTransaction transaction =
composedWorldStateStorage.startTransaction(); composedWorldStateStorage.startTransaction();
// TODO: consider ARCHIVE mode
transaction.put( transaction.put(
TRIE_BRANCH_STORAGE, FLAT_DB_MODE, FlatDbMode.FULL.getVersion().toArrayUnsafe()); TRIE_BRANCH_STORAGE, FLAT_DB_MODE, FlatDbMode.FULL.getVersion().toArrayUnsafe());
transaction.commit(); transaction.commit();
@ -291,7 +304,7 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
@Override @Override
public void clear() { public void clear() {
subscribers.forEach(BonsaiStorageSubscriber::onClearStorage); subscribers.forEach(BonsaiStorageSubscriber::onClearStorage);
getFlatDbReaderStrategy().clearAll(composedWorldStateStorage); getFlatDbStrategy().clearAll(composedWorldStateStorage);
composedWorldStateStorage.clear(TRIE_BRANCH_STORAGE); composedWorldStateStorage.clear(TRIE_BRANCH_STORAGE);
trieLogStorage.clear(); trieLogStorage.clear();
loadFlatDbStrategy(); // force reload of flat db reader strategy loadFlatDbStrategy(); // force reload of flat db reader strategy
@ -306,13 +319,15 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
@Override @Override
public void clearFlatDatabase() { public void clearFlatDatabase() {
subscribers.forEach(BonsaiStorageSubscriber::onClearFlatDatabaseStorage); subscribers.forEach(BonsaiStorageSubscriber::onClearFlatDatabaseStorage);
getFlatDbReaderStrategy().resetOnResync(composedWorldStateStorage); getFlatDbStrategy().resetOnResync(composedWorldStateStorage);
} }
@Override @Override
public BonsaiUpdater updater() { public BonsaiUpdater updater() {
return new Updater( return new Updater(
composedWorldStateStorage.startTransaction(), trieLogStorage.startTransaction()); composedWorldStateStorage.startTransaction(),
trieLogStorage.startTransaction(),
flatDbStrategy);
} }
@Override @Override
@ -351,18 +366,21 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
private final SegmentedKeyValueStorageTransaction composedWorldStateTransaction; private final SegmentedKeyValueStorageTransaction composedWorldStateTransaction;
private final KeyValueStorageTransaction trieLogStorageTransaction; private final KeyValueStorageTransaction trieLogStorageTransaction;
private final FlatDbStrategy flatDbStrategy;
public Updater( public Updater(
final SegmentedKeyValueStorageTransaction composedWorldStateTransaction, final SegmentedKeyValueStorageTransaction composedWorldStateTransaction,
final KeyValueStorageTransaction trieLogStorageTransaction) { final KeyValueStorageTransaction trieLogStorageTransaction,
final FlatDbStrategy flatDbStrategy) {
this.composedWorldStateTransaction = composedWorldStateTransaction; this.composedWorldStateTransaction = composedWorldStateTransaction;
this.trieLogStorageTransaction = trieLogStorageTransaction; this.trieLogStorageTransaction = trieLogStorageTransaction;
this.flatDbStrategy = flatDbStrategy;
} }
@Override @Override
public BonsaiUpdater removeCode(final Hash accountHash) { public BonsaiUpdater removeCode(final Hash accountHash) {
composedWorldStateTransaction.remove(CODE_STORAGE, accountHash.toArrayUnsafe()); flatDbStrategy.removeFlatCode(composedWorldStateTransaction, accountHash);
return this; return this;
} }
@ -372,14 +390,13 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
// Don't save empty values // Don't save empty values
return this; return this;
} }
composedWorldStateTransaction.put( flatDbStrategy.putFlatCode(composedWorldStateTransaction, accountHash, codeHash, code);
CODE_STORAGE, accountHash.toArrayUnsafe(), code.toArrayUnsafe());
return this; return this;
} }
@Override @Override
public BonsaiUpdater removeAccountInfoState(final Hash accountHash) { public BonsaiUpdater removeAccountInfoState(final Hash accountHash) {
composedWorldStateTransaction.remove(ACCOUNT_INFO_STATE, accountHash.toArrayUnsafe()); flatDbStrategy.removeFlatAccount(composedWorldStateTransaction, accountHash);
return this; return this;
} }
@ -389,8 +406,7 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
// Don't save empty values // Don't save empty values
return this; return this;
} }
composedWorldStateTransaction.put( flatDbStrategy.putFlatAccount(composedWorldStateTransaction, accountHash, accountValue);
ACCOUNT_INFO_STATE, accountHash.toArrayUnsafe(), accountValue.toArrayUnsafe());
return this; return this;
} }
@ -441,18 +457,16 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC
@Override @Override
public synchronized BonsaiUpdater putStorageValueBySlotHash( public synchronized BonsaiUpdater putStorageValueBySlotHash(
final Hash accountHash, final Hash slotHash, final Bytes storage) { final Hash accountHash, final Hash slotHash, final Bytes storage) {
composedWorldStateTransaction.put( flatDbStrategy.putFlatAccountStorageValueByStorageSlotHash(
ACCOUNT_STORAGE_STORAGE, composedWorldStateTransaction, accountHash, slotHash, storage);
Bytes.concatenate(accountHash, slotHash).toArrayUnsafe(),
storage.toArrayUnsafe());
return this; return this;
} }
@Override @Override
public synchronized void removeStorageValueBySlotHash( public synchronized void removeStorageValueBySlotHash(
final Hash accountHash, final Hash slotHash) { final Hash accountHash, final Hash slotHash) {
composedWorldStateTransaction.remove( flatDbStrategy.removeFlatAccountStorageValueByStorageSlotHash(
ACCOUNT_STORAGE_STORAGE, Bytes.concatenate(accountHash, slotHash).toArrayUnsafe()); composedWorldStateTransaction, accountHash, slotHash);
} }
@Override @Override

@ -26,6 +26,7 @@ import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage; import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorageTransaction;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -40,11 +41,11 @@ import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.rlp.RLP; import org.apache.tuweni.rlp.RLP;
/** /**
* This class represents a FlatDbReaderStrategy, which is responsible for reading data from flat * This class represents a FlatDbReaderStrategy, which is responsible for reading and writing data
* databases. It implements various methods for retrieving account data, code data, and storage data * from flat databases. It implements various methods for storing and retrieving account data, code
* from the corresponding KeyValueStorage. * data, and storage data from the corresponding KeyValueStorage.
*/ */
public abstract class FlatDbReaderStrategy { public abstract class FlatDbStrategy {
protected final MetricsSystem metricsSystem; protected final MetricsSystem metricsSystem;
protected final Counter getAccountCounter; protected final Counter getAccountCounter;
@ -53,7 +54,7 @@ public abstract class FlatDbReaderStrategy {
protected final Counter getStorageValueCounter; protected final Counter getStorageValueCounter;
protected final Counter getStorageValueFlatDatabaseCounter; protected final Counter getStorageValueFlatDatabaseCounter;
public FlatDbReaderStrategy(final MetricsSystem metricsSystem) { public FlatDbStrategy(final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem; this.metricsSystem = metricsSystem;
getAccountCounter = getAccountCounter =
@ -84,7 +85,7 @@ public abstract class FlatDbReaderStrategy {
/* /*
* Retrieves the account data for the given account hash, using the world state root hash supplier and node loader. * Retrieves the account data for the given account hash, using the world state root hash supplier and node loader.
*/ */
public abstract Optional<Bytes> getAccount( public abstract Optional<Bytes> getFlatAccount(
Supplier<Optional<Bytes>> worldStateRootHashSupplier, Supplier<Optional<Bytes>> worldStateRootHashSupplier,
NodeLoader nodeLoader, NodeLoader nodeLoader,
Hash accountHash, Hash accountHash,
@ -94,7 +95,7 @@ public abstract class FlatDbReaderStrategy {
* Retrieves the storage value for the given account hash and storage slot key, using the world state root hash supplier, storage root supplier, and node loader. * Retrieves the storage value for the given account hash and storage slot key, using the world state root hash supplier, storage root supplier, and node loader.
*/ */
public abstract Optional<Bytes> getStorageValueByStorageSlotKey( public abstract Optional<Bytes> getFlatStorageValueByStorageSlotKey(
Supplier<Optional<Bytes>> worldStateRootHashSupplier, Supplier<Optional<Bytes>> worldStateRootHashSupplier,
Supplier<Optional<Hash>> storageRootSupplier, Supplier<Optional<Hash>> storageRootSupplier,
NodeLoader nodeLoader, NodeLoader nodeLoader,
@ -105,7 +106,7 @@ public abstract class FlatDbReaderStrategy {
/* /*
* Retrieves the code data for the given code hash and account hash. * Retrieves the code data for the given code hash and account hash.
*/ */
public Optional<Bytes> getCode( public Optional<Bytes> getFlatCode(
final Bytes32 codeHash, final Hash accountHash, final SegmentedKeyValueStorage storage) { final Bytes32 codeHash, final Hash accountHash, final SegmentedKeyValueStorage storage) {
if (codeHash.equals(Hash.EMPTY)) { if (codeHash.equals(Hash.EMPTY)) {
return Optional.of(Bytes.EMPTY); return Optional.of(Bytes.EMPTY);
@ -117,6 +118,65 @@ public abstract class FlatDbReaderStrategy {
} }
} }
/*
* Puts the account data for the given account hash, using the world state root hash supplier and node loader.
*/
public void putFlatAccount(
final SegmentedKeyValueStorageTransaction transaction,
final Hash accountHash,
final Bytes accountValue) {
transaction.put(ACCOUNT_INFO_STATE, accountHash.toArrayUnsafe(), accountValue.toArrayUnsafe());
}
public void removeFlatAccount(
final SegmentedKeyValueStorageTransaction transaction, final Hash accountHash) {
transaction.remove(ACCOUNT_INFO_STATE, accountHash.toArrayUnsafe());
}
/*
* Puts the storage value for the given account hash and storage slot key, using the world state root hash supplier, storage root supplier, and node loader.
*/
public void putFlatAccountStorageValueByStorageSlotHash(
final SegmentedKeyValueStorageTransaction transaction,
final Hash accountHash,
final Hash slotHash,
final Bytes storage) {
transaction.put(
ACCOUNT_STORAGE_STORAGE,
Bytes.concatenate(accountHash, slotHash).toArrayUnsafe(),
storage.toArrayUnsafe());
}
/*
* Removes the storage value for the given account hash and storage slot key, using the world state root hash supplier, storage root supplier, and node loader.
*/
public void removeFlatAccountStorageValueByStorageSlotHash(
final SegmentedKeyValueStorageTransaction transaction,
final Hash accountHash,
final Hash slotHash) {
transaction.remove(
ACCOUNT_STORAGE_STORAGE, Bytes.concatenate(accountHash, slotHash).toArrayUnsafe());
}
/*
* Removes code for the given account hash.
*/
public void removeFlatCode(
final SegmentedKeyValueStorageTransaction transaction, final Hash accountHash) {
transaction.remove(CODE_STORAGE, accountHash.toArrayUnsafe());
}
/*
* Puts the code data for the given code hash and account hash.
*/
public void putFlatCode(
final SegmentedKeyValueStorageTransaction transaction,
final Hash accountHash,
final Bytes32 codeHash,
final Bytes code) {
transaction.put(CODE_STORAGE, accountHash.toArrayUnsafe(), code.toArrayUnsafe());
}
public void clearAll(final SegmentedKeyValueStorage storage) { public void clearAll(final SegmentedKeyValueStorage storage) {
storage.clear(ACCOUNT_INFO_STATE); storage.clear(ACCOUNT_INFO_STATE);
storage.clear(ACCOUNT_STORAGE_STORAGE); storage.clear(ACCOUNT_STORAGE_STORAGE);

@ -31,13 +31,13 @@ import java.util.function.Supplier;
import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes;
public class FullFlatDbReaderStrategy extends FlatDbReaderStrategy { public class FullFlatDbStrategy extends FlatDbStrategy {
protected final Counter getAccountNotFoundInFlatDatabaseCounter; protected final Counter getAccountNotFoundInFlatDatabaseCounter;
protected final Counter getStorageValueNotFoundInFlatDatabaseCounter; protected final Counter getStorageValueNotFoundInFlatDatabaseCounter;
public FullFlatDbReaderStrategy(final MetricsSystem metricsSystem) { public FullFlatDbStrategy(final MetricsSystem metricsSystem) {
super(metricsSystem); super(metricsSystem);
getAccountNotFoundInFlatDatabaseCounter = getAccountNotFoundInFlatDatabaseCounter =
@ -54,7 +54,7 @@ public class FullFlatDbReaderStrategy extends FlatDbReaderStrategy {
} }
@Override @Override
public Optional<Bytes> getAccount( public Optional<Bytes> getFlatAccount(
final Supplier<Optional<Bytes>> worldStateRootHashSupplier, final Supplier<Optional<Bytes>> worldStateRootHashSupplier,
final NodeLoader nodeLoader, final NodeLoader nodeLoader,
final Hash accountHash, final Hash accountHash,
@ -71,7 +71,7 @@ public class FullFlatDbReaderStrategy extends FlatDbReaderStrategy {
} }
@Override @Override
public Optional<Bytes> getStorageValueByStorageSlotKey( public Optional<Bytes> getFlatStorageValueByStorageSlotKey(
final Supplier<Optional<Bytes>> worldStateRootHashSupplier, final Supplier<Optional<Bytes>> worldStateRootHashSupplier,
final Supplier<Optional<Hash>> storageRootSupplier, final Supplier<Optional<Hash>> storageRootSupplier,
final NodeLoader nodeLoader, final NodeLoader nodeLoader,

@ -44,7 +44,7 @@ import org.apache.tuweni.rlp.RLP;
* methods, which checks if the data is present in the flat database, and if not, queries the merkle * methods, which checks if the data is present in the flat database, and if not, queries the merkle
* trie * trie
*/ */
public class PartialFlatDbReaderStrategy extends FlatDbReaderStrategy { public class PartialFlatDbStrategy extends FlatDbStrategy {
protected final Counter getAccountMerkleTrieCounter; protected final Counter getAccountMerkleTrieCounter;
protected final Counter getAccountMissingMerkleTrieCounter; protected final Counter getAccountMissingMerkleTrieCounter;
@ -52,7 +52,7 @@ public class PartialFlatDbReaderStrategy extends FlatDbReaderStrategy {
protected final Counter getStorageValueMerkleTrieCounter; protected final Counter getStorageValueMerkleTrieCounter;
protected final Counter getStorageValueMissingMerkleTrieCounter; protected final Counter getStorageValueMissingMerkleTrieCounter;
public PartialFlatDbReaderStrategy(final MetricsSystem metricsSystem) { public PartialFlatDbStrategy(final MetricsSystem metricsSystem) {
super(metricsSystem); super(metricsSystem);
getAccountMerkleTrieCounter = getAccountMerkleTrieCounter =
metricsSystem.createCounter( metricsSystem.createCounter(
@ -80,7 +80,7 @@ public class PartialFlatDbReaderStrategy extends FlatDbReaderStrategy {
} }
@Override @Override
public Optional<Bytes> getAccount( public Optional<Bytes> getFlatAccount(
final Supplier<Optional<Bytes>> worldStateRootHashSupplier, final Supplier<Optional<Bytes>> worldStateRootHashSupplier,
final NodeLoader nodeLoader, final NodeLoader nodeLoader,
final Hash accountHash, final Hash accountHash,
@ -111,7 +111,7 @@ public class PartialFlatDbReaderStrategy extends FlatDbReaderStrategy {
} }
@Override @Override
public Optional<Bytes> getStorageValueByStorageSlotKey( public Optional<Bytes> getFlatStorageValueByStorageSlotKey(
final Supplier<Optional<Bytes>> worldStateRootHashSupplier, final Supplier<Optional<Bytes>> worldStateRootHashSupplier,
final Supplier<Optional<Hash>> storageRootSupplier, final Supplier<Optional<Hash>> storageRootSupplier,
final NodeLoader nodeLoader, final NodeLoader nodeLoader,

@ -498,7 +498,9 @@ public class BonsaiWorldState
@Override @Override
public Hash frontierRootHash() { public Hash frontierRootHash() {
return calculateRootHash( return calculateRootHash(
Optional.of(new BonsaiWorldStateKeyValueStorage.Updater(noOpSegmentedTx, noOpTx)), Optional.of(
new BonsaiWorldStateKeyValueStorage.Updater(
noOpSegmentedTx, noOpTx, worldStateStorage.getFlatDbStrategy())),
accumulator.copy()); accumulator.copy());
} }

@ -12,7 +12,7 @@
* *
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
package org.hyperledger.besu.ethereum.bonsai; package org.hyperledger.besu.ethereum.bonsai.storage;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage.WORLD_ROOT_HASH_KEY; import static org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage.WORLD_ROOT_HASH_KEY;
@ -26,7 +26,6 @@ import static org.mockito.Mockito.verify;
import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.StorageSlotKey; import org.hyperledger.besu.datatypes.StorageSlotKey;
import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage;
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider;
import org.hyperledger.besu.ethereum.core.TrieGenerator; import org.hyperledger.besu.ethereum.core.TrieGenerator;
import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.rlp.RLP;
@ -372,7 +371,7 @@ public class BonsaiWorldStateKeyValueStorageTest {
// clear // clear
storage.clear(); storage.clear();
assertThat(storage.getFlatDbReaderStrategy()).isNotNull(); assertThat(storage.getFlatDbStrategy()).isNotNull();
assertThat(storage.getAccount(Hash.ZERO)).isEmpty(); assertThat(storage.getAccount(Hash.ZERO)).isEmpty();
} }

@ -69,7 +69,7 @@ Calculated : ${currentHash}
tasks.register('checkAPIChanges', FileStateChecker) { tasks.register('checkAPIChanges', FileStateChecker) {
description = "Checks that the API for the Plugin-API project does not change without deliberate thought" description = "Checks that the API for the Plugin-API project does not change without deliberate thought"
files = sourceSets.main.allJava.files files = sourceSets.main.allJava.files
knownHash = 'tpSnjt4HgqSiOTJhBbYdB0r1nFX4QZbicjfloI71Wf0=' knownHash = 'BhqPyj1fT50NWuHTgzgCmW1ynAPj/2QiGWraq5OwgOQ='
} }
check.dependsOn('checkAPIChanges') check.dependsOn('checkAPIChanges')

@ -23,6 +23,7 @@ import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.tuweni.bytes.Bytes;
/** Service provided by Besu to facilitate persistent data storage. */ /** Service provided by Besu to facilitate persistent data storage. */
public interface SegmentedKeyValueStorage extends Closeable { public interface SegmentedKeyValueStorage extends Closeable {
@ -37,6 +38,18 @@ public interface SegmentedKeyValueStorage extends Closeable {
*/ */
Optional<byte[]> get(SegmentIdentifier segment, byte[] key) throws StorageException; Optional<byte[]> get(SegmentIdentifier segment, byte[] key) throws StorageException;
/**
* Find the key and corresponding value "nearest to" the specified key. Nearest is defined as
* either matching the supplied key or the key lexicographically prior to it.
*
* @param segmentIdentifier segment to scan
* @param key key for which we are searching for the nearest match.
* @return Optional of NearestKeyValue-wrapped matched key and corresponding value.
* @throws StorageException the storage exception
*/
Optional<NearestKeyValue> getNearestTo(final SegmentIdentifier segmentIdentifier, Bytes key)
throws StorageException;
/** /**
* Contains key. * Contains key.
* *
@ -144,4 +157,22 @@ public interface SegmentedKeyValueStorage extends Closeable {
* @return boolean indicating whether the underlying storage is closed. * @return boolean indicating whether the underlying storage is closed.
*/ */
boolean isClosed(); boolean isClosed();
/**
* record type used to wrap responses from getNearestTo, includes the matched key and the value.
*
* @param key the matched (nearest) key
* @param value the corresponding value
*/
record NearestKeyValue(Bytes key, Optional<byte[]> value) {
/**
* Convenience method to map the Optional value to Bytes.
*
* @return Optional of Bytes.
*/
public Optional<Bytes> wrapBytes() {
return value.map(Bytes::wrap);
}
}
} }

@ -33,8 +33,11 @@ import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.tuweni.bytes.Bytes;
import org.rocksdb.AbstractRocksIterator;
import org.rocksdb.ColumnFamilyHandle; import org.rocksdb.ColumnFamilyHandle;
import org.rocksdb.OptimisticTransactionDB; import org.rocksdb.OptimisticTransactionDB;
import org.rocksdb.RocksIterator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -73,6 +76,18 @@ public class RocksDBColumnarKeyValueSnapshot
return snapTx.get(segment, key); return snapTx.get(segment, key);
} }
@Override
public Optional<NearestKeyValue> getNearestTo(
final SegmentIdentifier segmentIdentifier, final Bytes key) throws StorageException {
try (final RocksIterator rocksIterator = snapTx.getIterator(segmentIdentifier)) {
rocksIterator.seekForPrev(key.toArrayUnsafe());
return Optional.of(rocksIterator)
.filter(AbstractRocksIterator::isValid)
.map(it -> new NearestKeyValue(Bytes.of(it.key()), Optional.of(it.value())));
}
}
@Override @Override
public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segment) { public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segment) {
throwIfClosed(); throwIfClosed();

@ -44,6 +44,7 @@ import com.google.common.base.Splitter;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes;
import org.rocksdb.AbstractRocksIterator;
import org.rocksdb.BlockBasedTableConfig; import org.rocksdb.BlockBasedTableConfig;
import org.rocksdb.BloomFilter; import org.rocksdb.BloomFilter;
import org.rocksdb.ColumnFamilyDescriptor; import org.rocksdb.ColumnFamilyDescriptor;
@ -318,6 +319,19 @@ public abstract class RocksDBColumnarKeyValueStorage implements SegmentedKeyValu
} }
} }
@Override
public Optional<NearestKeyValue> getNearestTo(
final SegmentIdentifier segmentIdentifier, final Bytes key) throws StorageException {
try (final RocksIterator rocksIterator =
getDB().newIterator(safeColumnHandle(segmentIdentifier))) {
rocksIterator.seekForPrev(key.toArrayUnsafe());
return Optional.of(rocksIterator)
.filter(AbstractRocksIterator::isValid)
.map(it -> new NearestKeyValue(Bytes.of(it.key()), Optional.of(it.value())));
}
}
@Override @Override
public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segmentIdentifier) { public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segmentIdentifier) {
final RocksIterator rocksIterator = getDB().newIterator(safeColumnHandle(segmentIdentifier)); final RocksIterator rocksIterator = getDB().newIterator(safeColumnHandle(segmentIdentifier));

@ -137,6 +137,19 @@ public class RocksDBSnapshotTransaction
} }
} }
/**
* get a RocksIterator that reads through the transaction to represent the current state.
*
* <p>be sure to close this iterator, like in a try-with-resources block, otherwise a native
* memory leak might occur.
*
* @param segmentId id for the segment to iterate over.
* @return RocksIterator
*/
public RocksIterator getIterator(final SegmentIdentifier segmentId) {
return snapTx.getIterator(readOptions, columnFamilyMapper.apply(segmentId));
}
/** /**
* Stream. * Stream.
* *

@ -32,7 +32,7 @@ import org.apache.tuweni.bytes.Bytes;
*/ */
public class InMemoryKeyValueStorage extends SegmentedKeyValueStorageAdapter { public class InMemoryKeyValueStorage extends SegmentedKeyValueStorageAdapter {
private static final SegmentIdentifier SEGMENT_IDENTIFIER = static final SegmentIdentifier SEGMENT_IDENTIFIER =
new SegmentIdentifier() { new SegmentIdentifier() {
private static final String NAME = "SEGMENT_IDENTIFIER"; private static final String NAME = "SEGMENT_IDENTIFIER";

@ -93,6 +93,26 @@ public class LayeredKeyValueStorage extends SegmentedInMemoryKeyValueStorage
} }
} }
@Override
public Optional<NearestKeyValue> getNearestTo(
final SegmentIdentifier segmentIdentifier, final Bytes key) throws StorageException {
Optional<NearestKeyValue> ourNearest = super.getNearestTo(segmentIdentifier, key);
Optional<NearestKeyValue> parentNearest = parent.getNearestTo(segmentIdentifier, key);
if (ourNearest.isPresent() && parentNearest.isPresent()) {
// Both are present, return the one closer to the key
int ourDistance = ourNearest.get().key().commonPrefixLength(key);
int parentDistance = parentNearest.get().key().commonPrefixLength(key);
return (ourDistance <= parentDistance) ? ourNearest : parentNearest;
} else if (ourNearest.isPresent()) {
// Only ourNearest is present
return ourNearest;
} else {
// return parentNearest, which may be an empty Optional
return parentNearest;
}
}
@Override @Override
public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segmentId) { public Stream<Pair<byte[], byte[]>> stream(final SegmentIdentifier segmentId) {
throwIfClosed(); throwIfClosed();

@ -24,6 +24,7 @@ import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage;
import org.hyperledger.besu.plugin.services.storage.SnappedKeyValueStorage; import org.hyperledger.besu.plugin.services.storage.SnappedKeyValueStorage;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -113,6 +114,32 @@ public class SegmentedInMemoryKeyValueStorage
} }
} }
@Override
public Optional<NearestKeyValue> getNearestTo(
final SegmentIdentifier segmentIdentifier, final Bytes key) throws StorageException {
final Lock lock = rwLock.readLock();
lock.lock();
try {
// TODO: revisit this for sort performance
Comparator<Map.Entry<Bytes, Optional<byte[]>>> comparing =
Comparator.comparing(
(Map.Entry<Bytes, Optional<byte[]>> a) -> a.getKey().commonPrefixLength(key))
.thenComparing(Map.Entry.comparingByKey());
return this.hashValueStore
.computeIfAbsent(segmentIdentifier, s -> new HashMap<>())
.entrySet()
.stream()
// only return keys equal to or less than
.filter(e -> e.getKey().compareTo(key) <= 0)
.sorted(comparing.reversed())
.findFirst()
.map(z -> new NearestKeyValue(z.getKey(), z.getValue()));
} finally {
lock.unlock();
}
}
@Override @Override
public Set<byte[]> getAllKeysThat( public Set<byte[]> getAllKeysThat(
final SegmentIdentifier segmentIdentifier, final Predicate<byte[]> returnCondition) { final SegmentIdentifier segmentIdentifier, final Predicate<byte[]> returnCondition) {

@ -51,6 +51,10 @@ public class SegmentedKeyValueStorageAdapter implements KeyValueStorage {
this.storage = storage; this.storage = storage;
} }
SegmentedKeyValueStorage getSegmentedStore() {
return this.storage;
}
@Override @Override
public void clear() { public void clear() {
throwIfClosed(); throwIfClosed();

@ -0,0 +1,82 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
package org.hyperledger.besu.services.kvstore;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage.SEGMENT_IDENTIFIER;
import org.hyperledger.besu.kvstore.AbstractKeyValueStorageTest;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorageTransaction;
import java.util.stream.IntStream;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
public abstract class AbstractSegmentedKeyValueStorageTest extends AbstractKeyValueStorageTest {
public abstract SegmentedKeyValueStorage createSegmentedStore();
@Test
public void assertSegmentedIsNearestTo() throws Exception {
try (final var store = this.createSegmentedStore()) {
// create 10 entries
final SegmentedKeyValueStorageTransaction tx = store.startTransaction();
IntStream.range(1, 10)
.forEach(
i -> {
final byte[] key = bytesFromHexString("000" + i);
final byte[] value = bytesFromHexString("0FFF");
tx.put(SEGMENT_IDENTIFIER, key, value);
// different common prefix, and reversed order of bytes:
final byte[] key2 = bytesFromHexString("010" + (10 - i));
final byte[] value2 = bytesFromHexString("0FFF");
tx.put(SEGMENT_IDENTIFIER, key2, value2);
});
tx.commit();
// assert 0009 is closest to 000F
var val = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("000F"));
assertThat(val).isPresent();
assertThat(val.get().key()).isEqualTo(Bytes.fromHexString("0009"));
// assert 0109 is closest to 010D
var val2 = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("010D"));
assertThat(val2).isPresent();
assertThat(val2.get().key()).isEqualTo(Bytes.fromHexString("0109"));
// assert 0103 is closest to 0103
var val3 = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("0103"));
assertThat(val3).isPresent();
assertThat(val3.get().key()).isEqualTo(Bytes.fromHexString("0103"));
// assert 0003 is closest to 0003
var val4 = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("0003"));
assertThat(val4).isPresent();
assertThat(val4.get().key()).isEqualTo(Bytes.fromHexString("0003"));
// assert 0001 is closest to 0001
var val5 = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("0001"));
assertThat(val5).isPresent();
assertThat(val5.get().key()).isEqualTo(Bytes.fromHexString("0001"));
// assert 0000 is not present
var val6 = store.getNearestTo(SEGMENT_IDENTIFIER, Bytes.fromHexString("0000"));
assertThat(val6).isNotPresent();
}
}
}

@ -14,13 +14,18 @@
*/ */
package org.hyperledger.besu.services.kvstore; package org.hyperledger.besu.services.kvstore;
import org.hyperledger.besu.kvstore.AbstractKeyValueStorageTest;
import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; import org.hyperledger.besu.plugin.services.storage.KeyValueStorage;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage;
public class InMemoryKeyValueStorageTest extends AbstractKeyValueStorageTest { public class InMemoryKeyValueStorageTest extends AbstractSegmentedKeyValueStorageTest {
@Override @Override
protected KeyValueStorage createStore() { protected KeyValueStorage createStore() {
return new InMemoryKeyValueStorage(); return new InMemoryKeyValueStorage();
} }
@Override
public SegmentedKeyValueStorage createSegmentedStore() {
return new SegmentedInMemoryKeyValueStorage();
}
} }

@ -0,0 +1,33 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
package org.hyperledger.besu.services.kvstore;
import static org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage.SEGMENT_IDENTIFIER;
import org.hyperledger.besu.plugin.services.storage.KeyValueStorage;
import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage;
public class LayeredKeyValueStorageTest extends AbstractSegmentedKeyValueStorageTest {
@Override
protected KeyValueStorage createStore() {
return new SegmentedKeyValueStorageAdapter(SEGMENT_IDENTIFIER, createSegmentedStore());
}
@Override
public SegmentedKeyValueStorage createSegmentedStore() {
return new LayeredKeyValueStorage(new SegmentedInMemoryKeyValueStorage());
}
}
Loading…
Cancel
Save