diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index bace4a9e..8d0462ce 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -923,6 +923,11 @@ public class HSQLDBDatabaseUpdates { stmt.execute("ALTER TABLE Accounts DROP initial_level"); 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: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index ec615cc2..b0359218 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -500,9 +500,18 @@ public class HSQLDBRepository implements Repository { try (PreparedStatement preparedStatement = this.prepareStatement(sql)) { prepareExecute(preparedStatement, objects); + long beforeQuery = System.currentTimeMillis(); + if (preparedStatement.execute()) 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(); if (rowCount == -1) throw new SQLException("Database returned invalid row count"); @@ -516,7 +525,7 @@ public class HSQLDBRepository implements Repository { *

* Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column. *

- * 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 * @throws SQLException @@ -557,12 +566,9 @@ public class HSQLDBRepository implements Repository { sql.append(whereClause); sql.append(" LIMIT 1"); - try (PreparedStatement preparedStatement = this.prepareStatement(sql.toString()); - ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects)) { - if (resultSet == null) - return false; - - return true; + try (ResultSet resultSet = this.checkedExecute(sql.toString(), objects)) { + // If matching row is found then resultSet will not be null + return resultSet != null; } } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java index cbd5460f..8561b698 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java @@ -31,7 +31,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory { * * @param connectionUrl * @throws DataException without throwable if repository in use by another process. - * @throws DataException with throwable if repository cannot be opened for someother reason. + * @throws DataException with throwable if repository cannot be opened for some other reason. */ public HSQLDBRepositoryFactory(String connectionUrl) throws DataException { // one-time initialization goes in here @@ -47,10 +47,10 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory { HsqlException he = (HsqlException) cause; 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) - throw new DataException("Unable to open repository: " + e.getMessage(), e); + throw new DataException("Unable to read repository: " + e.getMessage(), e); // Attempt recovery? HSQLDBRepository.attemptRecovery(connectionUrl); diff --git a/src/test/java/org/qortal/test/RepositoryTests.java b/src/test/java/org/qortal/test/RepositoryTests.java index e06706dc..55cff418 100644 --- a/src/test/java/org/qortal/test/RepositoryTests.java +++ b/src/test/java/org/qortal/test/RepositoryTests.java @@ -15,6 +15,9 @@ import static org.junit.Assert.*; import java.math.BigDecimal; 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.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 { // Execute query to prime caches 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)); final long threshold = 3; // ms - assertTrue( isFast ? executionTime < threshold : executionTime > threshold); + assertTrue( !isFast || executionTime < threshold); } }