Skip pruning if trie log count is less than retention limit (#6463)

Rework error messaging in this case.

Signed-off-by: Simon Dudley <simon.dudley@consensys.net>
pull/6466/head
Simon Dudley 10 months ago committed by GitHub
parent 317d2ac6f4
commit 6ac419bf0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 73
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelper.java
  2. 93
      besu/src/test/java/org/hyperledger/besu/cli/subcommands/storage/TrieLogHelperTest.java

@ -16,7 +16,9 @@
package org.hyperledger.besu.cli.subcommands.storage;
import static com.google.common.base.Preconditions.checkArgument;
import static org.hyperledger.besu.cli.options.stable.DataStorageOptions.BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD;
import static org.hyperledger.besu.controller.BesuController.DATABASE_PATH;
import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.DEFAULT_BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE;
import org.hyperledger.besu.cli.options.stable.DataStorageOptions;
import org.hyperledger.besu.datatypes.Hash;
@ -74,11 +76,16 @@ public class TrieLogHelper {
final long lastBlockNumberToRetainTrieLogsFor = chainHeight - layersToRetain + 1;
if (!validPruneRequirements(blockchain, chainHeight, lastBlockNumberToRetainTrieLogsFor)) {
if (!validatePruneRequirements(
blockchain,
chainHeight,
lastBlockNumberToRetainTrieLogsFor,
rootWorldStateStorage,
layersToRetain)) {
return;
}
final long numberOfBatches = calculateNumberofBatches(layersToRetain);
final long numberOfBatches = calculateNumberOfBatches(layersToRetain);
processTrieLogBatches(
rootWorldStateStorage,
@ -88,11 +95,23 @@ public class TrieLogHelper {
numberOfBatches,
batchFileNameBase);
if (rootWorldStateStorage.streamTrieLogKeys(layersToRetain).count() == layersToRetain) {
deleteFiles(batchFileNameBase, numberOfBatches);
LOG.info("Prune ran successfully. Enjoy some disk space back! \uD83D\uDE80");
// Should only be layersToRetain left but loading extra just in case of an unforeseen bug
final long countAfterPrune =
rootWorldStateStorage
.streamTrieLogKeys(layersToRetain + DEFAULT_BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE)
.count();
if (countAfterPrune == layersToRetain) {
if (deleteFiles(batchFileNameBase, numberOfBatches)) {
LOG.info("Prune ran successfully. Enjoy some disk space back! \uD83D\uDE80");
} else {
throw new IllegalStateException(
"There was an error deleting the trie log backup files. Please ensure besu is working before deleting them manually.");
}
} else {
LOG.error("Prune failed. Re-run the subcommand to load the trie logs from file.");
throw new IllegalStateException(
String.format(
"Remaining trie logs (%d) did not match %s (%d). Trie logs backup files have not been deleted, it is safe to rerun the subcommand.",
countAfterPrune, BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD, layersToRetain));
}
}
@ -151,15 +170,21 @@ public class TrieLogHelper {
}
}
private void deleteFiles(final String batchFileNameBase, final long numberOfBatches) {
private boolean deleteFiles(final String batchFileNameBase, final long numberOfBatches) {
LOG.info("Deleting files...");
for (long batchNumber = 1; batchNumber <= numberOfBatches; batchNumber++) {
File file = new File(batchFileNameBase + "-" + batchNumber);
if (file.exists()) {
file.delete();
try {
for (long batchNumber = 1; batchNumber <= numberOfBatches; batchNumber++) {
File file = new File(batchFileNameBase + "-" + batchNumber);
if (file.exists()) {
file.delete();
}
}
return true;
} catch (Exception e) {
LOG.error("Error deleting files", e);
return false;
}
}
@ -177,14 +202,17 @@ public class TrieLogHelper {
return trieLogKeys;
}
private long calculateNumberofBatches(final long layersToRetain) {
private long calculateNumberOfBatches(final long layersToRetain) {
return layersToRetain / BATCH_SIZE + ((layersToRetain % BATCH_SIZE == 0) ? 0 : 1);
}
private boolean validPruneRequirements(
private boolean validatePruneRequirements(
final MutableBlockchain blockchain,
final long chainHeight,
final long lastBlockNumberToRetainTrieLogsFor) {
final long lastBlockNumberToRetainTrieLogsFor,
final BonsaiWorldStateKeyValueStorage rootWorldStateStorage,
final long layersToRetain) {
if (lastBlockNumberToRetainTrieLogsFor < 0) {
throw new IllegalArgumentException(
"Trying to retain more trie logs than chain length ("
@ -192,6 +220,19 @@ public class TrieLogHelper {
+ "), skipping pruning");
}
// Need to ensure we're loading at least layersToRetain if they exist
// plus extra threshold to account forks and orphans
final long clampedCountBeforePruning =
rootWorldStateStorage
.streamTrieLogKeys(layersToRetain + DEFAULT_BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE)
.count();
if (clampedCountBeforePruning < layersToRetain) {
throw new IllegalArgumentException(
String.format(
"Trie log count (%d) is less than retention limit (%d), skipping pruning",
clampedCountBeforePruning, layersToRetain));
}
final Optional<Hash> finalizedBlockHash = blockchain.getFinalized();
if (finalizedBlockHash.isEmpty()) {
@ -250,7 +291,7 @@ public class TrieLogHelper {
config.getBonsaiMaxLayersToLoad()
>= DataStorageConfiguration.Unstable.MINIMUM_BONSAI_TRIE_LOG_RETENTION_LIMIT,
String.format(
DataStorageOptions.BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD + " minimum value is %d",
BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD + " minimum value is %d",
DataStorageConfiguration.Unstable.MINIMUM_BONSAI_TRIE_LOG_RETENTION_LIMIT));
checkArgument(
config.getUnstable().getBonsaiTrieLogPruningWindowSize() > 0,
@ -264,7 +305,7 @@ public class TrieLogHelper {
String.format(
DataStorageOptions.Unstable.BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE
+ "=%d must be greater than "
+ DataStorageOptions.BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD
+ BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD
+ "=%d",
config.getUnstable().getBonsaiTrieLogPruningWindowSize(),
config.getBonsaiMaxLayersToLoad()));

@ -18,8 +18,10 @@ package org.hyperledger.besu.cli.subcommands.storage;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.DEFAULT_BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE;
import static org.hyperledger.besu.ethereum.worldstate.DataStorageFormat.BONSAI;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.datatypes.Hash;
@ -44,6 +46,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
@ -58,7 +61,7 @@ class TrieLogHelperTest {
private static final StorageProvider storageProvider = new InMemoryKeyValueStorageProvider();
private static BonsaiWorldStateKeyValueStorage inMemoryWorldState;
private TrieLogHelper trieLogHelper;
private TrieLogHelper nonValidatingTrieLogHelper;
private static class NonValidatingTrieLogHelper extends TrieLogHelper {
@Override
@ -106,7 +109,7 @@ class TrieLogHelperTest {
.put(blockHeader5.getHash().toArrayUnsafe(), createTrieLog(blockHeader5));
updater.getTrieLogStorageTransaction().commit();
trieLogHelper = new NonValidatingTrieLogHelper();
nonValidatingTrieLogHelper = new NonValidatingTrieLogHelper();
}
private static byte[] createTrieLog(final BlockHeader blockHeader) {
@ -150,7 +153,8 @@ class TrieLogHelperTest {
assertThat(inMemoryWorldState.getTrieLog(blockHeader3.getHash()).get())
.isEqualTo(createTrieLog(blockHeader3));
trieLogHelper.prune(dataStorageConfiguration, inMemoryWorldState, blockchain, dataDir);
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldState, blockchain, dataDir);
// assert pruned trie logs are not in the DB
assertThat(inMemoryWorldState.getTrieLog(blockHeader1.getHash())).isEqualTo(Optional.empty());
@ -182,7 +186,7 @@ class TrieLogHelperTest {
assertThatThrownBy(
() ->
trieLogHelper.prune(
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldState, blockchain, Path.of("")))
.isInstanceOf(RuntimeException.class)
.hasMessage("No finalized block present, can't safely run trie log prune");
@ -204,7 +208,7 @@ class TrieLogHelperTest {
assertThatThrownBy(
() ->
trieLogHelper.prune(
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldState, blockchain, Path.of("")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Trying to retain more trie logs than chain length (5), skipping pruning");
@ -227,13 +231,70 @@ class TrieLogHelperTest {
assertThatThrownBy(
() ->
trieLogHelper.prune(
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldState, blockchain, dataDir))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Trying to prune more layers than the finalized block height, skipping pruning");
}
@Test
public void skipPruningIfTrieLogCountIsLessThanMaxLayersToLoad() {
DataStorageConfiguration dataStorageConfiguration =
ImmutableDataStorageConfiguration.builder()
.dataStorageFormat(BONSAI)
.bonsaiMaxLayersToLoad(6L)
.unstable(
ImmutableDataStorageConfiguration.Unstable.builder()
.bonsaiLimitTrieLogsEnabled(true)
.build())
.build();
when(blockchain.getChainHeadBlockNumber()).thenReturn(5L);
assertThatThrownBy(
() ->
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldState, blockchain, Path.of("")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Trie log count (5) is less than retention limit (6), skipping pruning");
}
@Test
public void mismatchInPrunedTrieLogCountShouldNotDeleteFiles(final @TempDir Path dataDir)
throws IOException {
Files.createDirectories(dataDir.resolve("database"));
DataStorageConfiguration dataStorageConfiguration =
ImmutableDataStorageConfiguration.builder()
.dataStorageFormat(BONSAI)
.bonsaiMaxLayersToLoad(3L)
.unstable(
ImmutableDataStorageConfiguration.Unstable.builder()
.bonsaiLimitTrieLogsEnabled(true)
.build())
.build();
mockBlockchainBase();
when(blockchain.getBlockHeader(5)).thenReturn(Optional.of(blockHeader5));
when(blockchain.getBlockHeader(4)).thenReturn(Optional.of(blockHeader4));
when(blockchain.getBlockHeader(3)).thenReturn(Optional.of(blockHeader3));
final BonsaiWorldStateKeyValueStorage inMemoryWorldStateSpy = spy(inMemoryWorldState);
// force a different value the second time the trie log count is called
when(inMemoryWorldStateSpy.streamTrieLogKeys(3L + DEFAULT_BONSAI_TRIE_LOG_PRUNING_WINDOW_SIZE))
.thenCallRealMethod()
.thenReturn(Stream.empty());
assertThatThrownBy(
() ->
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration, inMemoryWorldStateSpy, blockchain, dataDir))
.isInstanceOf(RuntimeException.class)
.hasMessage(
"Remaining trie logs (0) did not match --bonsai-historical-block-limit (3). Trie logs backup files have not been deleted, it is safe to rerun the subcommand.");
}
@Test
public void trieLogRetentionLimitShouldBeAboveMinimum() {
@ -319,7 +380,7 @@ class TrieLogHelperTest {
assertThatThrownBy(
() ->
trieLogHelper.prune(
nonValidatingTrieLogHelper.prune(
dataStorageConfiguration,
inMemoryWorldState,
blockchain,
@ -342,13 +403,13 @@ class TrieLogHelperTest {
@Test
public void exportedTrieMatchesDbTrieLog(final @TempDir Path dataDir) throws IOException {
trieLogHelper.exportTrieLog(
nonValidatingTrieLogHelper.exportTrieLog(
inMemoryWorldState,
singletonList(blockHeader1.getHash()),
dataDir.resolve("trie-log-dump"));
var trieLog =
trieLogHelper
nonValidatingTrieLogHelper
.readTrieLogsAsRlpFromFile(dataDir.resolve("trie-log-dump").toString())
.entrySet()
.stream()
@ -362,13 +423,13 @@ class TrieLogHelperTest {
@Test
public void exportedMultipleTriesMatchDbTrieLogs(final @TempDir Path dataDir) throws IOException {
trieLogHelper.exportTrieLog(
nonValidatingTrieLogHelper.exportTrieLog(
inMemoryWorldState,
List.of(blockHeader1.getHash(), blockHeader2.getHash(), blockHeader3.getHash()),
dataDir.resolve("trie-log-dump"));
var trieLogs =
trieLogHelper
nonValidatingTrieLogHelper
.readTrieLogsAsRlpFromFile(dataDir.resolve("trie-log-dump").toString())
.entrySet()
.stream()
@ -389,13 +450,14 @@ class TrieLogHelperTest {
new BonsaiWorldStateKeyValueStorage(
tempStorageProvider, new NoOpMetricsSystem(), DataStorageConfiguration.DEFAULT_CONFIG);
trieLogHelper.exportTrieLog(
nonValidatingTrieLogHelper.exportTrieLog(
inMemoryWorldState,
singletonList(blockHeader1.getHash()),
dataDir.resolve("trie-log-dump"));
var trieLog =
trieLogHelper.readTrieLogsAsRlpFromFile(dataDir.resolve("trie-log-dump").toString());
nonValidatingTrieLogHelper.readTrieLogsAsRlpFromFile(
dataDir.resolve("trie-log-dump").toString());
var updater = inMemoryWorldState2.updater();
trieLog.forEach((k, v) -> updater.getTrieLogStorageTransaction().put(k, v));
@ -413,13 +475,14 @@ class TrieLogHelperTest {
new BonsaiWorldStateKeyValueStorage(
tempStorageProvider, new NoOpMetricsSystem(), DataStorageConfiguration.DEFAULT_CONFIG);
trieLogHelper.exportTrieLog(
nonValidatingTrieLogHelper.exportTrieLog(
inMemoryWorldState,
List.of(blockHeader1.getHash(), blockHeader2.getHash(), blockHeader3.getHash()),
dataDir.resolve("trie-log-dump"));
var trieLog =
trieLogHelper.readTrieLogsAsRlpFromFile(dataDir.resolve("trie-log-dump").toString());
nonValidatingTrieLogHelper.readTrieLogsAsRlpFromFile(
dataDir.resolve("trie-log-dump").toString());
var updater = inMemoryWorldState2.updater();
trieLog.forEach((k, v) -> updater.getTrieLogStorageTransaction().put(k, v));

Loading…
Cancel
Save