3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-12 18:25:49 +00:00

Added INDEX to speed up block orphaning.

NOTE: first startup after this commit can take a while due to building index!

SQL statement "DELETE FROM HistoricAccountBalances WHERE height >= ?" required
a full table scan and so was very slow on VMs/routers. This statement used by
HSQLDBAccountRepository.deleteBalancesFromHeight(), itself called during
Block.orphan().

Symptoms particularly evident during shutdown where above statement could take
upwards of 15 minutes on single-CPU, small-memory VMs!

Statement wasn't noticed before as slow-query checking wasn't involved.
Slow-query checking now applies to HSQLDBRepository.delete() and
HSQLDBRepository.exists() calls.

Added test for correct HSQLDB interrupt handling.

Fixed some typos.
This commit is contained in:
catbref 2020-02-14 12:09:49 +00:00
parent 0e9bbfe6fa
commit a4e127c84a
4 changed files with 65 additions and 11 deletions

View File

@ -923,6 +923,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE Accounts DROP initial_level"); stmt.execute("ALTER TABLE Accounts DROP initial_level");
break; break;
case 65:
// Add INDEX to speed up very slow "DELETE FROM HistoricAccountBalances WHERE height >= ?"
stmt.execute("CREATE INDEX IF NOT EXISTS HistoricAccountBalancesHeightIndex ON HistoricAccountBalances (height)");
break;
default: default:
// nothing to do // nothing to do
return false; return false;

View File

@ -500,9 +500,18 @@ public class HSQLDBRepository implements Repository {
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) { try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
prepareExecute(preparedStatement, objects); prepareExecute(preparedStatement, objects);
long beforeQuery = System.currentTimeMillis();
if (preparedStatement.execute()) if (preparedStatement.execute())
throw new SQLException("Database produced results, not row count"); throw new SQLException("Database produced results, not row count");
long queryTime = System.currentTimeMillis() - beforeQuery;
if (this.slowQueryThreshold != null && queryTime > this.slowQueryThreshold) {
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
logStatements();
}
int rowCount = preparedStatement.getUpdateCount(); int rowCount = preparedStatement.getUpdateCount();
if (rowCount == -1) if (rowCount == -1)
throw new SQLException("Database returned invalid row count"); throw new SQLException("Database returned invalid row count");
@ -516,7 +525,7 @@ public class HSQLDBRepository implements Repository {
* <p> * <p>
* Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column. * Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column.
* <p> * <p>
* Typically used after INSERTing NULL as the IDENTIY column's value to fetch what value was actually stored by HSQLDB. * Typically used after INSERTing NULL as the IDENTITY column's value to fetch what value was actually stored by HSQLDB.
* *
* @return Long * @return Long
* @throws SQLException * @throws SQLException
@ -557,12 +566,9 @@ public class HSQLDBRepository implements Repository {
sql.append(whereClause); sql.append(whereClause);
sql.append(" LIMIT 1"); sql.append(" LIMIT 1");
try (PreparedStatement preparedStatement = this.prepareStatement(sql.toString()); try (ResultSet resultSet = this.checkedExecute(sql.toString(), objects)) {
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects)) { // If matching row is found then resultSet will not be null
if (resultSet == null) return resultSet != null;
return false;
return true;
} }
} }

View File

@ -31,7 +31,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
* *
* @param connectionUrl * @param connectionUrl
* @throws DataException <i>without throwable</i> if repository in use by another process. * @throws DataException <i>without throwable</i> if repository in use by another process.
* @throws DataException <i>with throwable</i> if repository cannot be opened for someother reason. * @throws DataException <i>with throwable</i> if repository cannot be opened for some other reason.
*/ */
public HSQLDBRepositoryFactory(String connectionUrl) throws DataException { public HSQLDBRepositoryFactory(String connectionUrl) throws DataException {
// one-time initialization goes in here // one-time initialization goes in here
@ -47,10 +47,10 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
HsqlException he = (HsqlException) cause; HsqlException he = (HsqlException) cause;
if (he.getErrorCode() == -ErrorCode.LOCK_FILE_ACQUISITION_FAILURE) if (he.getErrorCode() == -ErrorCode.LOCK_FILE_ACQUISITION_FAILURE)
throw new DataException("Unable to open repository: " + e.getMessage()); throw new DataException("Unable to lock repository: " + e.getMessage());
if (he.getErrorCode() != -ErrorCode.ERROR_IN_LOG_FILE && he.getErrorCode() != -ErrorCode.M_DatabaseScriptReader_read) if (he.getErrorCode() != -ErrorCode.ERROR_IN_LOG_FILE && he.getErrorCode() != -ErrorCode.M_DatabaseScriptReader_read)
throw new DataException("Unable to open repository: " + e.getMessage(), e); throw new DataException("Unable to read repository: " + e.getMessage(), e);
// Attempt recovery? // Attempt recovery?
HSQLDBRepository.attemptRecovery(connectionUrl); HSQLDBRepository.attemptRecovery(connectionUrl);

View File

@ -15,6 +15,9 @@ import static org.junit.Assert.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -126,6 +129,46 @@ public class RepositoryTests extends Common {
} }
} }
/** Test proper action of interrupt inside an HSQLDB statement. */
@Test
public void testInterrupt() {
try (final Repository repository = RepositoryManager.getRepository()) {
final Thread testThread = Thread.currentThread();
System.out.println(String.format("Thread ID: %s", testThread.getId()));
// Queue interrupt
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.schedule(() -> testThread.interrupt(), 1000L, TimeUnit.MILLISECONDS);
// Set rollback on interrupt
@SuppressWarnings("resource")
final HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
hsqldb.prepareStatement("SET DATABASE TRANSACTION ROLLBACK ON INTERRUPT TRUE").execute();
// Create SQL procedure that calls hsqldbSleep() to block HSQLDB so we can interrupt()
hsqldb.prepareStatement("CREATE PROCEDURE sleep(IN millis INT) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.test.RepositoryTests.hsqldbSleep'").execute();
// Execute long-running statement
hsqldb.prepareStatement("CALL sleep(2000)").execute();
if (!testThread.isInterrupted())
// We should not reach here
fail("Interrupt was swallowed");
} catch (DataException | SQLException e) {
fail("DataException during blocked statement");
}
}
public static void hsqldbSleep(int millis) throws SQLException {
System.out.println(String.format("HSQLDB sleep() thread ID: %s", Thread.currentThread().getId()));
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void testSql(HSQLDBRepository hsqldb, String sql, boolean isFast) throws DataException, SQLException { private void testSql(HSQLDBRepository hsqldb, String sql, boolean isFast) throws DataException, SQLException {
// Execute query to prime caches // Execute query to prime caches
hsqldb.prepareStatement(sql).execute(); hsqldb.prepareStatement(sql).execute();
@ -138,7 +181,7 @@ public class RepositoryTests extends Common {
System.out.println(String.format("%s: [%d ms] SQL: %s", (isFast ? "fast": "slow"), executionTime, sql)); System.out.println(String.format("%s: [%d ms] SQL: %s", (isFast ? "fast": "slow"), executionTime, sql));
final long threshold = 3; // ms final long threshold = 3; // ms
assertTrue( isFast ? executionTime < threshold : executionTime > threshold); assertTrue( !isFast || executionTime < threshold);
} }
} }