diff --git a/src/main/java/org/qortal/controller/repository/BlockArchiver.java b/src/main/java/org/qortal/controller/repository/BlockArchiver.java index d6860347..15b9b226 100644 --- a/src/main/java/org/qortal/controller/repository/BlockArchiver.java +++ b/src/main/java/org/qortal/controller/repository/BlockArchiver.java @@ -27,6 +27,13 @@ public class BlockArchiver implements Runnable { try (final Repository repository = RepositoryManager.getRepository()) { int startHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight(); + // Don't attempt to archive if we have no ATStatesHeightIndex, as it will be too slow + boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex(); + if (!hasAtStatesHeightIndex) { + LOGGER.info("Unable to start block archiver due to missing ATStatesHeightIndex. Bootstrapping is recommended."); + return; + } + // Don't even start building until initial rush has ended Thread.sleep(INITIAL_SLEEP_PERIOD); diff --git a/src/main/java/org/qortal/controller/repository/BlockPruner.java b/src/main/java/org/qortal/controller/repository/BlockPruner.java index f8fd2195..f5be6ee8 100644 --- a/src/main/java/org/qortal/controller/repository/BlockPruner.java +++ b/src/main/java/org/qortal/controller/repository/BlockPruner.java @@ -34,6 +34,13 @@ public class BlockPruner implements Runnable { try (final Repository repository = RepositoryManager.getRepository()) { int pruneStartHeight = repository.getBlockRepository().getBlockPruneHeight(); + // Don't attempt to prune if we have no ATStatesHeightIndex, as it will be too slow + boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex(); + if (!hasAtStatesHeightIndex) { + LOGGER.info("Unable to start block pruner due to missing ATStatesHeightIndex. Bootstrapping is recommended."); + return; + } + while (!Controller.isStopping()) { repository.discardChanges(); diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java index 9316875d..0f537ae9 100644 --- a/src/main/java/org/qortal/repository/ATRepository.java +++ b/src/main/java/org/qortal/repository/ATRepository.java @@ -1,5 +1,7 @@ package org.qortal.repository; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; import java.util.Set; @@ -146,6 +148,10 @@ public interface ATRepository { public int pruneAtStates(int minHeight, int maxHeight) throws DataException; + /** Checks for the presence of the ATStatesHeightIndex in repository */ + public boolean hasAtStatesHeightIndex() throws DataException; + + /** * Save ATStateData into repository. *

diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java index f7557750..c392d213 100644 --- a/src/main/java/org/qortal/repository/RepositoryManager.java +++ b/src/main/java/org/qortal/repository/RepositoryManager.java @@ -61,11 +61,16 @@ public abstract class RepositoryManager { public static boolean archive() { // Bulk archive the database the first time we use archive mode if (Settings.getInstance().isArchiveEnabled()) { - try { - return HSQLDBDatabaseArchiving.buildBlockArchive(); + if (RepositoryManager.canArchiveOrPrune()) { + try { + return HSQLDBDatabaseArchiving.buildBlockArchive(); - } catch (DataException e) { - LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state."); + } catch (DataException e) { + LOGGER.info("Unable to build block archive. The database may have been left in an inconsistent state."); + } + } + else { + LOGGER.info("Unable to build block archive due to missing ATStatesHeightIndex. Bootstrapping is recommended."); } } return false; @@ -75,18 +80,23 @@ public abstract class RepositoryManager { // Bulk prune the database the first time we use pruning mode if (Settings.getInstance().isPruningEnabled() || Settings.getInstance().isArchiveEnabled()) { - try { - boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates(); - boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks(); + if (RepositoryManager.canArchiveOrPrune()) { + try { + boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates(); + boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks(); - // Perform repository maintenance to shrink the db size down - if (prunedATStates && prunedBlocks) { - HSQLDBDatabasePruning.performMaintenance(); - return true; + // Perform repository maintenance to shrink the db size down + if (prunedATStates && prunedBlocks) { + HSQLDBDatabasePruning.performMaintenance(); + return true; + } + + } catch (SQLException | DataException e) { + LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state."); } - - } catch (SQLException | DataException e) { - LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state."); + } + else { + LOGGER.info("Unable to prune blocks due to missing ATStatesHeightIndex. Bootstrapping is recommended."); } } return false; @@ -118,4 +128,12 @@ public abstract class RepositoryManager { return SQLException.class.isInstance(cause) && repositoryFactory.isDeadlockException((SQLException) cause); } + public static boolean canArchiveOrPrune() { + try (final Repository repository = getRepository()) { + return repository.getATRepository().hasAtStatesHeightIndex(); + } catch (DataException e) { + return false; + } + } + } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index 56658ec7..85196d31 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -798,6 +798,19 @@ public class HSQLDBATRepository implements ATRepository { } + @Override + public boolean hasAtStatesHeightIndex() throws DataException { + String sql = "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.SYSTEM_INDEXINFO where INDEX_NAME='ATSTATESHEIGHTINDEX'"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { + return resultSet != null; + + } catch (SQLException e) { + throw new DataException("Unable to check for ATStatesHeightIndex in repository", e); + } + } + + @Override public void save(ATStateData atStateData) throws DataException { // We shouldn't ever save partial ATStateData diff --git a/src/test/java/org/qortal/test/BlockArchiveTests.java b/src/test/java/org/qortal/test/BlockArchiveTests.java index c05915cd..373c98f2 100644 --- a/src/test/java/org/qortal/test/BlockArchiveTests.java +++ b/src/test/java/org/qortal/test/BlockArchiveTests.java @@ -17,6 +17,7 @@ import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.group.Group; import org.qortal.repository.*; +import org.qortal.repository.hsqldb.HSQLDBRepository; import org.qortal.settings.Settings; import org.qortal.test.common.AtUtils; import org.qortal.test.common.BlockUtils; @@ -33,6 +34,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Path; import java.nio.file.Paths; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -487,6 +489,29 @@ public class BlockArchiveTests extends Common { } + /** + * Many nodes are missing an ATStatesHeightIndex due to an earlier bug + * In these cases we disable archiving and pruning as this index is a + * very essential component in these processes. + */ + @Test + public void testMissingAtStatesHeightIndex() throws DataException, SQLException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + + // Firstly check that we're able to prune or archive when the index exists + assertTrue(repository.getATRepository().hasAtStatesHeightIndex()); + assertTrue(RepositoryManager.canArchiveOrPrune()); + + // Delete the index + repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute(); + + // Ensure check that we're unable to prune or archive when the index doesn't exist + assertFalse(repository.getATRepository().hasAtStatesHeightIndex()); + assertFalse(RepositoryManager.canArchiveOrPrune()); + } + } + + private void deleteArchiveDirectory() { // Delete archive directory if exists Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath();