From 1b4c75a76eb5980d0ad8da2bf7ed992cfedf205e Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 22 Aug 2021 15:17:46 +0100 Subject: [PATCH] Prune all blocks up until the blockPruneLimit By default, this leaves only the last 1450 blocks in the database. Only applies when pruning mode is enabled. --- .../controller/pruning/BlockPruner.java | 86 +++++++++++++++++++ .../controller/pruning/PruneManager.java | 1 + .../qortal/repository/BlockRepository.java | 14 +++ .../hsqldb/HSQLDBBlockRepository.java | 47 ++++++++++ .../hsqldb/HSQLDBDatabaseUpdates.java | 1 + .../java/org/qortal/settings/Settings.java | 49 +++++++---- 6 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/qortal/controller/pruning/BlockPruner.java diff --git a/src/main/java/org/qortal/controller/pruning/BlockPruner.java b/src/main/java/org/qortal/controller/pruning/BlockPruner.java new file mode 100644 index 00000000..8ae25224 --- /dev/null +++ b/src/main/java/org/qortal/controller/pruning/BlockPruner.java @@ -0,0 +1,86 @@ +package org.qortal.controller.pruning; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.controller.Controller; +import org.qortal.data.block.BlockData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.utils.NTP; + +public class BlockPruner implements Runnable { + + private static final Logger LOGGER = LogManager.getLogger(BlockPruner.class); + + @Override + public void run() { + Thread.currentThread().setName("Block pruner"); + + if (!Settings.getInstance().isPruningEnabled()) { + return; + } + + try (final Repository repository = RepositoryManager.getRepository()) { + int pruneStartHeight = repository.getBlockRepository().getBlockPruneHeight(); + + while (!Controller.isStopping()) { + repository.discardChanges(); + + Thread.sleep(Settings.getInstance().getBlockPruneInterval()); + + BlockData chainTip = Controller.getInstance().getChainTip(); + if (chainTip == null || NTP.getTime() == null) + continue; + + // Don't even attempt if we're mid-sync as our repository requests will be delayed for ages + if (Controller.getInstance().isSynchronizing()) + continue; + + // Prune all blocks up until our latest minus pruneBlockLimit + final int ourLatestHeight = chainTip.getHeight(); + final int upperPrunableHeight = ourLatestHeight - Settings.getInstance().getPruneBlockLimit(); + + int upperBatchHeight = pruneStartHeight + Settings.getInstance().getBlockPruneBatchSize(); + int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight); + + if (pruneStartHeight >= upperPruneHeight) { + continue; + } + + LOGGER.debug(String.format("Pruning blocks between %d and %d...", pruneStartHeight, upperPruneHeight)); + + int numBlocksPruned = repository.getBlockRepository().pruneBlocks(pruneStartHeight, upperPruneHeight); + repository.saveChanges(); + + if (numBlocksPruned > 0) { + final int finalPruneStartHeight = pruneStartHeight; + LOGGER.debug(() -> String.format("Pruned %d block%s between %d and %d", + numBlocksPruned, (numBlocksPruned != 1 ? "s" : ""), + finalPruneStartHeight, upperPruneHeight)); + } else { + // Can we move onto next batch? + if (upperPrunableHeight > upperBatchHeight) { + pruneStartHeight = upperBatchHeight; + repository.getBlockRepository().setBlockPruneHeight(pruneStartHeight); + repository.saveChanges(); + + final int finalPruneStartHeight = pruneStartHeight; + LOGGER.debug(() -> String.format("Bumping block base prune height to %d", finalPruneStartHeight)); + } + else { + // We've pruned up to the upper prunable height + // Back off for a while to save CPU for syncing + Thread.sleep(10*60*1000L); + } + } + } + } catch (DataException e) { + LOGGER.warn(String.format("Repository issue trying to prune blocks: %s", e.getMessage())); + } catch (InterruptedException e) { + // Time to exit + } + } + +} diff --git a/src/main/java/org/qortal/controller/pruning/PruneManager.java b/src/main/java/org/qortal/controller/pruning/PruneManager.java index dcd7391d..66019d01 100644 --- a/src/main/java/org/qortal/controller/pruning/PruneManager.java +++ b/src/main/java/org/qortal/controller/pruning/PruneManager.java @@ -23,6 +23,7 @@ public class PruneManager { // Start individual pruning processes ExecutorService pruneExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory()); pruneExecutor.execute(new AtStatesPruner()); + pruneExecutor.execute(new BlockPruner()); } public static synchronized PruneManager getInstance() { diff --git a/src/main/java/org/qortal/repository/BlockRepository.java b/src/main/java/org/qortal/repository/BlockRepository.java index 78eba399..5ca61e66 100644 --- a/src/main/java/org/qortal/repository/BlockRepository.java +++ b/src/main/java/org/qortal/repository/BlockRepository.java @@ -166,6 +166,20 @@ public interface BlockRepository { */ public BlockData getDetachedBlockSignature(int startHeight) throws DataException; + + /** Returns height of first prunable block. */ + public int getBlockPruneHeight() throws DataException; + + /** Sets new base height for block pruning. + *

+ * NOTE: performs implicit repository.saveChanges(). + */ + public void setBlockPruneHeight(int pruneHeight) throws DataException; + + /** Prunes full block data between passed heights. Returns number of pruned rows. */ + public int pruneBlocks(int minHeight, int maxHeight) throws DataException; + + /** * Saves block into repository. * diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java index b486e6a0..2f7e4ad2 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -509,6 +509,53 @@ public class HSQLDBBlockRepository implements BlockRepository { } } + + @Override + public int getBlockPruneHeight() throws DataException { + String sql = "SELECT block_prune_height FROM DatabaseInfo"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { + if (resultSet == null) + return 0; + + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to fetch block prune height from repository", e); + } + } + + @Override + public void setBlockPruneHeight(int pruneHeight) throws DataException { + // trimHeightsLock is to prevent concurrent update on DatabaseInfo + // that could result in "transaction rollback: serialization failure" + synchronized (this.repository.trimHeightsLock) { + String updateSql = "UPDATE DatabaseInfo SET block_prune_height = ?"; + + try { + this.repository.executeCheckedUpdate(updateSql, pruneHeight); + this.repository.saveChanges(); + } catch (SQLException e) { + repository.examineException(e); + throw new DataException("Unable to set block prune height in repository", e); + } + } + } + + @Override + public int pruneBlocks(int minHeight, int maxHeight) throws DataException { + // Don't prune the genesis block + if (minHeight <= 1) { + minHeight = 2; + } + + try { + return this.repository.delete("Blocks", "height BETWEEN ? AND ?", minHeight, maxHeight); + } catch (SQLException e) { + throw new DataException("Unable to prune blocks from repository", e); + } + } + + @Override public BlockData getDetachedBlockSignature(int startHeight) throws DataException { String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks " diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 94e753e8..d696351f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -870,6 +870,7 @@ public class HSQLDBDatabaseUpdates { case 35: // Support for pruning stmt.execute("ALTER TABLE DatabaseInfo ADD AT_prune_height INT NOT NULL DEFAULT 0"); + stmt.execute("ALTER TABLE DatabaseInfo ADD block_prune_height INT NOT NULL DEFAULT 0"); break; default: diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 9fe533b3..e1deb641 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -106,24 +106,32 @@ public class Settings { /** Max number of AT states to trim in one go. */ private int atStatesTrimLimit = 4000; // records - /** Whether we should prune old data to reduce database size - * This prevents the node from being able to serve older blocks */ - private boolean pruningEnabled = false; - /** The amount of recent blocks we should keep when pruning */ - private int pruneBlockLimit = 1440; - - /** How often to attempt AT state pruning (ms). */ - private long atStatesPruneInterval = 3219L; // milliseconds - /** Block height range to scan for trimmable AT states.
- * This has a significant effect on execution time. */ - private int atStatesPruneBatchSize = 10; // blocks - /** How often to attempt online accounts signatures trimming (ms). */ private long onlineSignaturesTrimInterval = 9876L; // milliseconds /** Block height range to scan for trimmable online accounts signatures.
* This has a significant effect on execution time. */ private int onlineSignaturesTrimBatchSize = 100; // blocks + + /** Whether we should prune old data to reduce database size + * This prevents the node from being able to serve older blocks */ + private boolean pruningEnabled = false; + /** The amount of recent blocks we should keep when pruning */ + private int pruneBlockLimit = 1450; + + /** How often to attempt AT state pruning (ms). */ + private long atStatesPruneInterval = 3219L; // milliseconds + /** Block height range to scan for prunable AT states.
+ * This has a significant effect on execution time. */ + private int atStatesPruneBatchSize = 10; // blocks + + /** How often to attempt block pruning (ms). */ + private long blockPruneInterval = 3219L; // milliseconds + /** Block height range to scan for prunable blocks.
+ * This has a significant effect on execution time. */ + private int blockPruneBatchSize = 10000; // blocks + + // Peer-to-peer related private boolean isTestNet = false; /** Port number for inbound peer-to-peer connections. */ @@ -540,6 +548,15 @@ public class Settings { return this.atStatesTrimLimit; } + public long getOnlineSignaturesTrimInterval() { + return this.onlineSignaturesTrimInterval; + } + + public int getOnlineSignaturesTrimBatchSize() { + return this.onlineSignaturesTrimBatchSize; + } + + public boolean isPruningEnabled() { return this.pruningEnabled; } @@ -556,12 +573,12 @@ public class Settings { return this.atStatesPruneBatchSize; } - public long getOnlineSignaturesTrimInterval() { - return this.onlineSignaturesTrimInterval; + public long getBlockPruneInterval() { + return this.blockPruneInterval; } - public int getOnlineSignaturesTrimBatchSize() { - return this.onlineSignaturesTrimBatchSize; + public int getBlockPruneBatchSize() { + return this.blockPruneBatchSize; } }