Transactional database interface
Currently the DB is guarded by a read-write lock: read-only transactions acquire the read lock and read-write transactions acquire the write lock. This ensures that we never get lock timeouts from the DB, so clients don't need to worry about rolling back and retrying transactions if there's a lock timeout. However, if a transaction performs long-running work such as verifying a signature, other transactions are prevented from accessing the database in the meantime.
We can avoid this by creating a transactional interface to the database, which encapsulates each transaction in a task object. Users of the database submit tasks, and the database is responsible for rolling back and retrying any tasks that time out. This allows us to remove the global read-write lock.
interface TransactionTask {
boolean run(Transaction txn) throws DbException;
}
The task's run()
method performs DB operations using the provided transaction and returns true if the transaction should be committed or false if it should be aborted. If the run()
method throws an exception, the transaction is aborted.
The DB has a method for running database tasks:
boolean runTask(TransactionTask task) throws DbException;
The database's runTask()
method starts a transaction, passes it to the task, and commits or aborts it as required. The method returns true if the transaction was committed, false if it was aborted, or rethrows an exception thrown by the task.
We can remove the DB's startTransaction()
and endTransaction()
methods, along with the transaction's setComplete()
method, since the DB itself will handle starting, committing and aborting transactions. It will no longer be necessary to specify whether a transaction is read-only or read-write.
Depends on #319.