-
Notifications
You must be signed in to change notification settings - Fork 839
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add --X-trie-log subcommand #6303
Changes from 12 commits
16c0a49
7dd4928
bf2b098
e67ae51
9b4e0c9
0b9fe83
426848e
7401b59
1b7fb72
11e6b05
f2d01e2
04f1aaa
2f01c5a
56e4c8e
78561b0
42c72cf
9961fc2
9389540
e3d4fbc
c7144fe
20b0ba5
e7d175c
b214bf2
aa75348
2bc0732
deec021
5d7b68e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
/* | ||
* Copyright contributors to Hyperledger Besu. | ||
* | ||
* 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.cli.subcommands.storage; | ||
|
||
import static com.google.common.base.Preconditions.checkArgument; | ||
import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; | ||
|
||
import org.hyperledger.besu.datatypes.Hash; | ||
import org.hyperledger.besu.ethereum.chain.Blockchain; | ||
import org.hyperledger.besu.ethereum.chain.MutableBlockchain; | ||
import org.hyperledger.besu.ethereum.core.BlockHeader; | ||
import org.hyperledger.besu.ethereum.trie.bonsai.storage.BonsaiWorldStateKeyValueStorage; | ||
import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.BufferedWriter; | ||
import java.io.File; | ||
import java.io.FileReader; | ||
import java.io.FileWriter; | ||
import java.io.IOException; | ||
import java.io.PrintWriter; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.IdentityHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
|
||
import com.google.common.base.Splitter; | ||
import org.apache.tuweni.bytes.Bytes; | ||
import org.apache.tuweni.bytes.Bytes32; | ||
import org.bouncycastle.util.encoders.Base64; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** Helper class for counting and pruning trie logs */ | ||
public class TrieLogHelper { | ||
private static final String trieLogFile = "trieLogsToRetain.txt"; | ||
private static final Logger LOG = LoggerFactory.getLogger(TrieLogHelper.class); | ||
|
||
static void prune( | ||
final PrintWriter out, | ||
final DataStorageConfiguration config, | ||
final BonsaiWorldStateKeyValueStorage rootWorldStateStorage, | ||
final MutableBlockchain blockchain) | ||
throws IOException { | ||
|
||
TrieLogHelper.validatePruneConfiguration(config); | ||
final long layersToRetain = config.getUnstable().getBonsaiTrieLogRetentionThreshold(); | ||
final long chainHeight = blockchain.getChainHeadBlockNumber(); | ||
final long lastBlockToRetainTrieLogsFor = chainHeight - layersToRetain; | ||
final Optional<Hash> finalizedBlockHash = blockchain.getFinalized(); | ||
|
||
if (finalizedBlockHash.isEmpty()) { | ||
LOG.error("No finalized block present, skipping pruning"); | ||
return; | ||
} else { | ||
if (blockchain.getBlockHeader(finalizedBlockHash.get()).get().getNumber() | ||
< lastBlockToRetainTrieLogsFor) { | ||
LOG.error("Trying to prune more layers than the finalized block height, skipping pruning"); | ||
return; | ||
} | ||
} | ||
|
||
// retrieve the layersToRetains hashes from blockchain | ||
final List<Hash> trieLogKeys = new ArrayList<>(); | ||
|
||
for (long i = chainHeight; i > lastBlockToRetainTrieLogsFor; i--) { | ||
final Optional<BlockHeader> header = blockchain.getBlockHeader(i); | ||
header.ifPresent(blockHeader -> trieLogKeys.add(blockHeader.getHash())); | ||
} | ||
|
||
IdentityHashMap<byte[], byte[]> trieLogsToRetain; | ||
|
||
// TODO: maybe stop the method here if we don't find enough hashes to retain | ||
if ((long) trieLogKeys.size() == layersToRetain) { | ||
trieLogsToRetain = new IdentityHashMap<>(); | ||
// save trielogs in a flatfile in case something goes wrong | ||
out.println("Obtaining trielogs to retain..."); | ||
trieLogKeys.forEach( | ||
hash -> { | ||
rootWorldStateStorage | ||
.getTrieLog(hash) | ||
.ifPresent(trieLog -> trieLogsToRetain.put(hash.toArrayUnsafe(), trieLog)); | ||
}); | ||
out.println("Saving trielogs to retain in file..."); | ||
saveTrieLogsInFile(trieLogsToRetain); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how the fail-safe is being used. Expected to see that if we got an error clearing and inserting entries we ready from the file or allow specifying an existing backup file to restore from. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment and instructions that will be logged for the user. Basically the idea here is, we retain the logs in a file in case something goes wrong. |
||
} else { | ||
// in case something went wrong and we already pruned trielogs | ||
// users can re-un the subcommand and we will read trielogs from file | ||
trieLogsToRetain = readTrieLogsFromFile(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't understand how reading from the file helps in this case. We didn't find all the hashes but also haven't created the file at this point either unless we are relying running the command again to read the previous state There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a failsafe in for a subsequent execution where something went terribly wrong during the truncation and restore-from-file process. what would be nice, but not idiomatic, is if we had a few "are you sure?" or "do you want to?" prompts that could be bypassed by |
||
} | ||
|
||
if (trieLogsToRetain.size() == layersToRetain) { | ||
out.println("Clear trielogs..."); | ||
rootWorldStateStorage.clearTrieLog(); | ||
out.println("Restoring trielogs retained into db..."); | ||
recreateTrieLogs(rootWorldStateStorage, trieLogsToRetain); | ||
} | ||
if (rootWorldStateStorage.streamTrieLogKeys(layersToRetain).count() == layersToRetain) { | ||
out.println("Prune ran successfully. Deleting file..."); | ||
deleteTrieLogFile(); | ||
out.println("Enjoy some GBs of storage back!..."); | ||
gfukushima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
out.println("Prune failed. Re-run the subcommand to load the trielogs from file."); | ||
} | ||
} | ||
|
||
private static void recreateTrieLogs( | ||
final BonsaiWorldStateKeyValueStorage rootWorldStateStorage, | ||
final IdentityHashMap<byte[], byte[]> trieLogsToRetain) { | ||
var updater = rootWorldStateStorage.updater(); | ||
|
||
trieLogsToRetain.forEach( | ||
(key, value) -> { | ||
updater.getTrieLogStorageTransaction().put(key, value); | ||
}); | ||
updater.getTrieLogStorageTransaction().commit(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this fails then do have a way of restoring the trielog from the backup? |
||
} | ||
|
||
private static void validatePruneConfiguration(final DataStorageConfiguration config) { | ||
checkArgument( | ||
config.getUnstable().getBonsaiTrieLogRetentionThreshold() | ||
>= MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD, | ||
String.format( | ||
"--Xbonsai-trie-log-retention-threshold minimum value is %d", | ||
MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD)); | ||
checkArgument( | ||
config.getUnstable().getBonsaiTrieLogPruningLimit() > 0, | ||
String.format( | ||
"--Xbonsai-trie-log-pruning-limit=%d must be greater than 0", | ||
config.getUnstable().getBonsaiTrieLogPruningLimit())); | ||
checkArgument( | ||
config.getUnstable().getBonsaiTrieLogPruningLimit() | ||
> config.getUnstable().getBonsaiTrieLogRetentionThreshold(), | ||
String.format( | ||
"--Xbonsai-trie-log-pruning-limit=%d must greater than --Xbonsai-trie-log-retention-threshold=%d", | ||
config.getUnstable().getBonsaiTrieLogPruningLimit(), | ||
config.getUnstable().getBonsaiTrieLogRetentionThreshold())); | ||
} | ||
|
||
static TrieLogCount getCount( | ||
final BonsaiWorldStateKeyValueStorage rootWorldStateStorage, | ||
final int limit, | ||
final Blockchain blockchain) { | ||
final AtomicInteger total = new AtomicInteger(); | ||
final AtomicInteger canonicalCount = new AtomicInteger(); | ||
final AtomicInteger forkCount = new AtomicInteger(); | ||
final AtomicInteger orphanCount = new AtomicInteger(); | ||
rootWorldStateStorage | ||
.streamTrieLogKeys(limit) | ||
.map(Bytes32::wrap) | ||
.map(Hash::wrap) | ||
.forEach( | ||
hash -> { | ||
total.getAndIncrement(); | ||
blockchain | ||
.getBlockHeader(hash) | ||
.ifPresentOrElse( | ||
(header) -> { | ||
long number = header.getNumber(); | ||
final Optional<BlockHeader> headerByNumber = | ||
blockchain.getBlockHeader(number); | ||
if (headerByNumber.isPresent() | ||
&& headerByNumber.get().getHash().equals(hash)) { | ||
canonicalCount.getAndIncrement(); | ||
} else { | ||
forkCount.getAndIncrement(); | ||
} | ||
}, | ||
orphanCount::getAndIncrement); | ||
}); | ||
|
||
return new TrieLogCount(total.get(), canonicalCount.get(), forkCount.get(), orphanCount.get()); | ||
} | ||
|
||
private static void saveTrieLogsInFile(final Map<byte[], byte[]> trieLogs) throws IOException { | ||
|
||
File file = new File(trieLogFile); | ||
|
||
try (BufferedWriter bf = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) { | ||
for (Map.Entry<byte[], byte[]> entry : trieLogs.entrySet()) { | ||
bf.write(Bytes.of(entry.getKey()) + ":" + Base64.toBase64String(entry.getValue())); | ||
bf.newLine(); | ||
} | ||
bf.flush(); | ||
} catch (IOException e) { | ||
LOG.error(e.getMessage()); | ||
throw e; | ||
} | ||
} | ||
|
||
private static IdentityHashMap<byte[], byte[]> readTrieLogsFromFile() throws IOException { | ||
|
||
File file = new File(trieLogFile); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wold be better to use File.createTempFile so it's created in the temp directory and we won't have any write permissions issues There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need the file to exist after we finish the run if for some reason we fail to reload the trielogs into the db |
||
IdentityHashMap<byte[], byte[]> trieLogs = new IdentityHashMap<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trielogs can vary in size, and the retention count is user configurable. We could easily blow up the heap with bad settings. We might want to add some sanity check on the file size here, and perhaps also in trielog streaming/save to file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, users could eventually set the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trielogs can be the basis of a specific worldstate for debug_ and trace_ endpoints. But really if that were the case I imagine the node would be an archive node. I think streaming to and from disk would likely just be more bulletproof. Not blocking feedback, just seems a robust mechanism There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed this so we process it in batches now so it should be a safer mechanism. |
||
try (BufferedReader br = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) { | ||
String line; | ||
while ((line = br.readLine()) != null) { | ||
List<String> parts = Splitter.on(':').splitToList(line); | ||
byte[] key = Bytes.fromHexString(parts.get(0)).toArrayUnsafe(); | ||
byte[] value = Base64.decode(parts.get(1)); | ||
trieLogs.put(key, value); | ||
} | ||
} catch (IOException e) { | ||
LOG.error(e.getMessage()); | ||
throw e; | ||
} | ||
|
||
return trieLogs; | ||
} | ||
|
||
private static void deleteTrieLogFile() { | ||
File file = new File(trieLogFile); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we create the file we could use the File.deleteOnExit so we don't need to worry about manually deleting this file |
||
if (file.exists()) { | ||
file.delete(); | ||
} | ||
} | ||
|
||
static void printCount(final PrintWriter out, final TrieLogCount count) { | ||
out.printf( | ||
"trieLog count: %s\n - canonical count: %s\n - fork count: %s\n - orphaned count: %s\n", | ||
count.total, count.canonicalCount, count.forkCount, count.orphanCount); | ||
} | ||
|
||
record TrieLogCount(int total, int canonicalCount, int forkCount, int orphanCount) {} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changes here seem fine but unrelated. Every time I look at this class name though I think it is a CLI Usage helper, like the output that gets printed when one asks for help or gets command usage wrong. completely unrelated, but if we are going to make changes in this pr ... :)