diff --git a/gradle.properties b/gradle.properties index 2d8d1e4dd150cb790e1efef522121e30222820d4..98026c56f7ececb04c7dc71c78524d831cf92b19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1g diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt index 336cac11ff218437b8dbc3e94287f33117a97b59..56e1dd9fa81c6d384e4b8a7a9a5440dfeca440c7 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt @@ -2,9 +2,8 @@ package org.briarproject.mailbox.core.db import org.briarproject.mailbox.core.api.Contact import org.briarproject.mailbox.core.settings.Settings -import java.sql.Connection -interface Database<T> { +interface Database : TransactionManager { /** * Opens the database and returns true if the database already existed. @@ -18,38 +17,19 @@ interface Database<T> { @Throws(DbException::class) fun close() - /** - * Starts a new transaction and returns an object representing it. - */ - @Throws(DbException::class) - fun startTransaction(): T - - /** - * Aborts the given transaction - no changes made during the transaction - * will be applied to the database. - */ - fun abortTransaction(txn: T) - - /** - * Commits the given transaction - all changes made during the transaction - * will be applied to the database. - */ - @Throws(DbException::class) - fun commitTransaction(txn: T) - @Throws(DbException::class) - fun getSettings(txn: Connection, namespace: String?): Settings + fun getSettings(txn: Transaction, namespace: String): Settings @Throws(DbException::class) - fun mergeSettings(txn: Connection, s: Settings, namespace: String?) + fun mergeSettings(txn: Transaction, s: Settings, namespace: String) @Throws(DbException::class) - fun addContact(txn: T, contact: Contact) + fun addContact(txn: Transaction, contact: Contact) @Throws(DbException::class) - fun getContact(txn: T, id: Int): Contact? + fun getContact(txn: Transaction, id: Int): Contact? @Throws(DbException::class) - fun removeContact(txn: T, id: Int) + fun removeContact(txn: Transaction, id: Int) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java deleted file mode 100644 index 88e0e248e8413ebe18e2e1728d623f167e35ce26..0000000000000000000000000000000000000000 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponent.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.briarproject.mailbox.core.db; - -import javax.annotation.Nullable; - -/** - * Encapsulates the database implementation and exposes high-level operations - * to other components. - */ -public interface DatabaseComponent { - - /** - * Opens the database and returns true if the database already existed. - */ - boolean open(@Nullable MigrationListener listener); - - /** - * Waits for any open transactions to finish and closes the database. - */ - void close(); - -} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt deleted file mode 100644 index e17f7014cce00c48304d7d938c39e52e3c78b8da..0000000000000000000000000000000000000000 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseComponentImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.briarproject.mailbox.core.db - -import javax.inject.Inject - -class DatabaseComponentImpl<T> @Inject constructor(database: Database<T>) : DatabaseComponent { - - private val db: Database<T>? = null - - override fun open(listener: MigrationListener?): Boolean { - // TODO: implement this - return true - } - - override fun close() { - // TODO: implement this - } - -} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt index 84a4e28308ca4866d54a27145043a1a7686b3820..e32d0fc766b3d5afeac82f95b08fed3652ef22db 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/DatabaseModule.kt @@ -5,7 +5,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.briarproject.mailbox.core.system.Clock -import java.sql.Connection import javax.inject.Singleton @Module @@ -14,14 +13,8 @@ internal class DatabaseModule { @Provides @Singleton - fun provideDatabase(config: DatabaseConfig, clock: Clock): Database<Connection> { + fun provideDatabase(config: DatabaseConfig, clock: Clock): Database { return H2Database(config, clock) } - @Provides - @Singleton - fun provideDatabaseComponent(db: Database<Connection>): DatabaseComponent { - return DatabaseComponentImpl(db) - } - } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt index a0433b4879e8653fca233adc7182d118393f1f44..b46c2d3409b5dbade799c805863fe47b0ece68ee 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt @@ -22,10 +22,11 @@ import java.util.Arrays import java.util.LinkedList import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock +import java.util.concurrent.locks.ReentrantReadWriteLock import javax.annotation.concurrent.GuardedBy abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val clock: Clock) : - Database<Connection> { + Database { internal companion object { @@ -64,6 +65,8 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc @Volatile private var wasDirtyOnInitialisation = false + private val lock = ReentrantReadWriteLock(true) + fun open(driverClass: String, reopen: Boolean, listener: MigrationListener?) { // Load the JDBC driver try { @@ -72,27 +75,23 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc throw DbException(e) } // Open the database and create the tables and indexes if necessary - val compact: Boolean - var txn = startTransaction() - try { + var compact = false + transaction(false) { txn -> + val connection = txn.unbox() compact = if (reopen) { - val s: Settings = getSettings(txn, DB_SETTINGS_NAMESPACE) + val s: Settings = getSettings(connection, DB_SETTINGS_NAMESPACE) wasDirtyOnInitialisation = isDirty(s) - migrateSchema(txn, s, listener) || isCompactionDue(s) + migrateSchema(connection, s, listener) || isCompactionDue(s) } else { wasDirtyOnInitialisation = false - createTables(txn) - initialiseSettings(txn) + createTables(connection) + initialiseSettings(connection) false } if (LOG.isInfoEnabled) { LOG.info("db dirty? $wasDirtyOnInitialisation") } - createIndexes(txn) - commitTransaction(txn) - } catch (e: DbException) { - abortTransaction(txn) - throw e + createIndexes(connection) } // Compact the database if necessary if (compact) { @@ -102,13 +101,8 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc logDuration(LOG, { "Compacting database" }, start) // Allow the next transaction to reopen the DB synchronized(connectionsLock) { closed = false } - txn = startTransaction() - try { - storeLastCompacted(txn) - commitTransaction(txn) - } catch (e: DbException) { - abortTransaction(txn) - throw e + transaction(false) { txn -> + storeLastCompacted(txn.unbox()) } } } @@ -126,7 +120,11 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc * current code and cannot be migrated */ @Throws(DbException::class) - private fun migrateSchema(txn: Connection, s: Settings, listener: MigrationListener?): Boolean { + private fun migrateSchema( + connection: Connection, + s: Settings, + listener: MigrationListener?, + ): Boolean { var dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1) if (dataSchemaVersion == -1) throw DbException() if (dataSchemaVersion == CODE_SCHEMA_VERSION) return false @@ -139,9 +137,9 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc if (LOG.isInfoEnabled) LOG.info("Migrating from schema $start to $end") listener?.onDatabaseMigration() // Apply the migration - m.migrate(txn) + m.migrate(connection) // Store the new schema version - storeSchemaVersion(txn, end) + storeSchemaVersion(connection, end) dataSchemaVersion = end } } @@ -162,20 +160,51 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc @Throws(DbException::class) protected abstract fun compactAndClose() - override fun startTransaction(): Connection { - var txn: Connection? + /** + * Starts a new transaction and returns an object representing it. + * + * This method acquires locks, so it must not be called while holding a + * lock. + * + * @param readOnly true if the transaction will only be used for reading. + */ + private fun startTransaction(readOnly: Boolean): Transaction { + // Don't allow reentrant locking + check(lock.readHoldCount <= 0) + check(lock.writeHoldCount <= 0) + val start = now() + if (readOnly) { + lock.readLock().lock() + logDuration(LOG, { "Waiting for read lock" }, start) + } else { + lock.writeLock().lock() + logDuration(LOG, { "Waiting for write lock" }, start) + } + return try { + Transaction(startTransaction(), readOnly) + } catch (e: DbException) { + if (readOnly) lock.readLock().unlock() else lock.writeLock().unlock() + throw e + } catch (e: RuntimeException) { + if (readOnly) lock.readLock().unlock() else lock.writeLock().unlock() + throw e + } + } + + private fun startTransaction(): Connection { + var connection: Connection? connectionsLock.lock() - txn = try { + connection = try { if (closed) throw DbClosedException() connections.poll() } finally { connectionsLock.unlock() } try { - if (txn == null) { + if (connection == null) { // Open a new connection - txn = createConnection() - txn.autoCommit = false + connection = createConnection() + connection.autoCommit = false connectionsLock.lock() try { openConnections++ @@ -186,15 +215,15 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } catch (e: SQLException) { throw DbException(e) } - return txn + return connection } - override fun abortTransaction(txn: Connection) { + private fun abortTransaction(connection: Connection) { try { - txn.rollback() + connection.rollback() connectionsLock.lock() try { - connections.add(txn) + connections.add(connection) connectionsChanged.signalAll() } finally { connectionsLock.unlock() @@ -202,7 +231,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } catch (e: SQLException) { // Try to close the connection logException(LOG, e) - tryToClose(txn, LOG) + tryToClose(connection, LOG) // Whatever happens, allow the database to close connectionsLock.lock() try { @@ -214,15 +243,15 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } } - override fun commitTransaction(txn: Connection) { + private fun commitTransaction(connection: Connection) { try { - txn.commit() + connection.commit() } catch (e: SQLException) { throw DbException(e) } connectionsLock.lock() try { - connections.add(txn) + connections.add(connection) connectionsChanged.signalAll() } finally { connectionsLock.unlock() @@ -260,10 +289,10 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - fun setDirty(txn: Connection, dirty: Boolean) { + fun setDirty(connection: Connection, dirty: Boolean) { val s = Settings() s.putBoolean(DIRTY_KEY, dirty) - mergeSettings(txn, s, DB_SETTINGS_NAMESPACE) + mergeSettings(connection, s, DB_SETTINGS_NAMESPACE) } private fun isCompactionDue(s: Settings): Boolean { @@ -274,32 +303,32 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - private fun storeSchemaVersion(txn: Connection, version: Int) { + private fun storeSchemaVersion(connection: Connection, version: Int) { val s = Settings() s.putInt(SCHEMA_VERSION_KEY, version) - mergeSettings(txn, s, DB_SETTINGS_NAMESPACE) + mergeSettings(connection, s, DB_SETTINGS_NAMESPACE) } @Throws(DbException::class) - private fun storeLastCompacted(txn: Connection) { + private fun storeLastCompacted(connection: Connection) { val s = Settings() s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis()) - mergeSettings(txn, s, DB_SETTINGS_NAMESPACE) + mergeSettings(connection, s, DB_SETTINGS_NAMESPACE) } @Throws(DbException::class) - private fun initialiseSettings(txn: Connection) { + private fun initialiseSettings(connection: Connection) { val s = Settings() s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION) s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis()) - mergeSettings(txn, s, DB_SETTINGS_NAMESPACE) + mergeSettings(connection, s, DB_SETTINGS_NAMESPACE) } @Throws(DbException::class) - private fun createTables(txn: Connection) { + private fun createTables(connection: Connection) { var s: Statement? = null try { - s = txn.createStatement() + s = connection.createStatement() s.executeUpdate(dbTypes.replaceTypes(CREATE_SETTINGS)) s.executeUpdate(dbTypes.replaceTypes(CREATE_CONTACTS)) s.close() @@ -310,10 +339,10 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - private fun createIndexes(txn: Connection) { + private fun createIndexes(connection: Connection) { var s: Statement? = null try { - s = txn.createStatement() + s = connection.createStatement() // s.executeUpdate(INDEX_SOMETABLE_BY_SOMECOLUMN) s.close() } catch (e: SQLException) { @@ -323,7 +352,13 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun getSettings(txn: Connection, namespace: String?): Settings { + override fun getSettings(txn: Transaction, namespace: String): Settings { + val connection: Connection = txn.unbox() as Connection + return getSettings(connection, namespace) + } + + @Throws(DbException::class) + private fun getSettings(connection: Connection, namespace: String): Settings { var ps: PreparedStatement? = null var rs: ResultSet? = null return try { @@ -332,7 +367,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc WHERE namespace = ? """.trimIndent() - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) ps.setString(1, namespace) rs = ps.executeQuery() val s = Settings() @@ -348,7 +383,13 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun mergeSettings(txn: Connection, s: Settings, namespace: String?) { + override fun mergeSettings(txn: Transaction, s: Settings, namespace: String) { + val connection: Connection = txn.unbox() as Connection + mergeSettings(connection, s, namespace) + } + + @Throws(DbException::class) + fun mergeSettings(connection: Connection, s: Settings, namespace: String) { var ps: PreparedStatement? = null try { // Update any settings that already exist @@ -357,7 +398,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc WHERE namespace = ? AND settingKey = ? """.trimIndent() - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) for ((key, value) in s) { ps.setString(1, value) ps.setString(2, namespace) @@ -376,7 +417,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc VALUES (?, ?, ?) """.trimIndent() - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) var updateIndex = 0 var inserted = 0 for ((key, value) in s) { @@ -400,13 +441,14 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun addContact(txn: Connection, contact: Contact) { + override fun addContact(txn: Transaction, contact: Contact) { + val connection: Connection = txn.unbox() as Connection var ps: PreparedStatement? = null try { val sql = """INSERT INTO contacts (contactId, token, inbox, outbox) VALUES (?, ?, ?, ?) """.trimIndent() - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) ps.setInt(1, contact.id) ps.setString(2, contact.token) ps.setString(3, contact.inboxId) @@ -421,14 +463,15 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun getContact(txn: Connection, id: Int): Contact? { + override fun getContact(txn: Transaction, id: Int): Contact? { + val connection: Connection = txn.unbox() as Connection var ps: PreparedStatement? = null var rs: ResultSet? = null try { val sql = """SELECT token, inbox, outbox FROM contacts WHERE contactId = ? """.trimIndent() - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) ps.setInt(1, id) rs = ps.executeQuery() if (!rs.next()) return null @@ -446,11 +489,12 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun removeContact(txn: Connection, id: Int) { + override fun removeContact(txn: Transaction, id: Int) { + val connection: Connection = txn.unbox() as Connection var ps: PreparedStatement? = null try { val sql = "DELETE FROM contacts WHERE contactId = ?" - ps = txn.prepareStatement(sql) + ps = connection.prepareStatement(sql) ps.setInt(1, id) val affected = ps.executeUpdate() if (affected != 1) throw DbStateException() @@ -461,4 +505,53 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } } + /** + * Commits a transaction to the database. + */ + @Throws(DbException::class) + private fun commitTransaction(txn: Transaction) { + val connection: Connection = txn.unbox() as Connection + check(!txn.isCommitted) + txn.setCommitted() + commitTransaction(connection) + } + + /** + * Ends a transaction. If the transaction has not been committed, + * it will be aborted. If the transaction has been committed, + * any events attached to the transaction are broadcast. + * The database lock will be released in either case. + */ + private fun endTransaction(txn: Transaction) { + try { + val connection: Connection = txn.unbox() as Connection + if (!txn.isCommitted) { + abortTransaction(connection) + } + } finally { + if (txn.isReadOnly) lock.readLock().unlock() else lock.writeLock().unlock() + } + } + + override fun transaction(readOnly: Boolean, task: (Transaction) -> Unit) { + val txn = startTransaction(readOnly) + try { + task(txn) + commitTransaction(txn) + } finally { + endTransaction(txn) + } + } + + override fun <R> transactionWithResult(readOnly: Boolean, task: (Transaction) -> R): R { + val txn = startTransaction(readOnly) + try { + val result = task(txn) + commitTransaction(txn) + return result + } finally { + endTransaction(txn) + } + } + } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Transaction.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Transaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..81176661a8f34e4d72cc40f9c5860be56ba19748 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Transaction.kt @@ -0,0 +1,35 @@ +package org.briarproject.mailbox.core.db + +import java.sql.Connection + +class Transaction( + private val txn: Connection, + /** + * Returns true if the transaction can only be used for reading. + */ + val isReadOnly: Boolean, +) { + + /** + * Returns true if the transaction has been committed. + */ + var isCommitted = false + private set + + /** + * Returns the database transaction. The type of the returned object + * depends on the database implementation. + */ + fun unbox(): Connection { + return txn + } + + /** + * Marks the transaction as committed. This method should only be called + * by the DatabaseComponent. It must not be called more than once. + */ + fun setCommitted() { + check(!isCommitted) + isCommitted = true + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..d663509b2d682e9d163075dd35ea438eb21f3ac8 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/TransactionManager.kt @@ -0,0 +1,17 @@ +package org.briarproject.mailbox.core.db + +interface TransactionManager { + + /** + * Runs the given task within a transaction. + */ + @Throws(DbException::class) + fun transaction(readOnly: Boolean, task: (Transaction) -> Unit) + + /** + * Runs the given task within a transaction and returns the result of the + * task. + */ + @Throws(DbException::class) + fun <R> transactionWithResult(readOnly: Boolean, task: (Transaction) -> R): R +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java index 63719354388f2fd8af55660a4e5ddf476b9219a8..2e8aa3ee71e0895b2cc056f5935972850c7482d0 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java @@ -1,6 +1,6 @@ package org.briarproject.mailbox.core.lifecycle; -import org.briarproject.mailbox.core.db.DatabaseComponent; +import org.briarproject.mailbox.core.db.Database; import org.briarproject.mailbox.core.system.Wakeful; import java.util.concurrent.ExecutorService; @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.StateFlow; /** * Manages the lifecycle of the app: opening and closing the - * {@link DatabaseComponent} starting and stopping {@link Service Services}, + * {@link Database} starting and stopping {@link Service Services}, * and shutting down {@link ExecutorService ExecutorServices}. */ public interface LifecycleManager { @@ -29,8 +29,13 @@ public interface LifecycleManager { */ enum LifecycleState { - STOPPED, STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES, - RUNNING, STOPPING; + STOPPED, + STARTING, + MIGRATING_DATABASE, + COMPACTING_DATABASE, + STARTING_SERVICES, + RUNNING, + STOPPING; public boolean isAfter(LifecycleState state) { return ordinal() > state.ordinal(); @@ -57,7 +62,7 @@ public interface LifecycleManager { void registerForShutdown(ExecutorService e); /** - * Opens the {@link DatabaseComponent} using the given key and starts any + * Opens the {@link Database} using the given key and starts any * registered {@link Service Services}. */ @Wakeful @@ -66,18 +71,18 @@ public interface LifecycleManager { /** * Stops any registered {@link Service Services}, shuts down any * registered {@link ExecutorService ExecutorServices}, and closes the - * {@link DatabaseComponent}. + * {@link Database}. */ @Wakeful void stopServices(); /** - * Waits for the {@link DatabaseComponent} to be opened before returning. + * Waits for the {@link Database} to be opened before returning. */ void waitForDatabase() throws InterruptedException; /** - * Waits for the {@link DatabaseComponent} to be opened and all registered + * Waits for the {@link Database} to be opened and all registered * {@link Service Services} to start before returning. */ void waitForStartup() throws InterruptedException; @@ -85,7 +90,7 @@ public interface LifecycleManager { /** * Waits for all registered {@link Service Services} to stop, all * registered {@link ExecutorService ExecutorServices} to shut down, and - * the {@link DatabaseComponent} to be closed before returning. + * the {@link Database} to be closed before returning. */ void waitForShutdown() throws InterruptedException; @@ -104,4 +109,4 @@ public interface LifecycleManager { @Wakeful void onDatabaseOpened(); } -} \ No newline at end of file +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt index d5e659d0c5c455419179f82bb52ea561e8edd515..1ab9b3549ed4d6196c59bee7807a8f02c49d6f72 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt @@ -2,7 +2,7 @@ package org.briarproject.mailbox.core.lifecycle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.briarproject.mailbox.core.db.DatabaseComponent +import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.db.MigrationListener import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE @@ -31,7 +31,7 @@ import javax.annotation.concurrent.ThreadSafe import javax.inject.Inject @ThreadSafe -internal class LifecycleManagerImpl @Inject constructor(private val db: DatabaseComponent) : +internal class LifecycleManagerImpl @Inject constructor(private val db: Database) : LifecycleManager, MigrationListener { companion object { diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..691517279963a38a37f96fc82337859ae76e34b4 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt @@ -0,0 +1,25 @@ +package org.briarproject.mailbox.core.settings + +import org.briarproject.mailbox.core.db.DbException +import org.briarproject.mailbox.core.db.Transaction + +interface SettingsManager { + /** + * Returns all settings in the given namespace. + */ + @Throws(DbException::class) + fun getSettings(namespace: String): Settings + + /** + * Returns all settings in the given namespace. + */ + @Throws(DbException::class) + fun getSettings(txn: Transaction, namespace: String): Settings + + /** + * Merges the given settings with any existing settings in the given + * namespace. + */ + @Throws(DbException::class) + fun mergeSettings(s: Settings, namespace: String) +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1ccc66733d9d8efd8f9782d23b742db83d6caf2 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt @@ -0,0 +1,28 @@ +package org.briarproject.mailbox.core.settings + +import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.db.DbException +import org.briarproject.mailbox.core.db.Transaction +import javax.annotation.concurrent.Immutable +import javax.inject.Inject + +@Immutable +internal class SettingsManagerImpl @Inject constructor(private val db: Database) : SettingsManager { + + @Throws(DbException::class) + override fun getSettings(namespace: String): Settings { + return db.transactionWithResult(true) { txn -> + db.getSettings(txn, namespace) + } + } + + @Throws(DbException::class) + override fun getSettings(txn: Transaction, namespace: String): Settings { + return db.getSettings(txn, namespace) + } + + @Throws(DbException::class) + override fun mergeSettings(s: Settings, namespace: String) { + db.transaction(false) { txn -> db.mergeSettings(txn, s, namespace) } + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..75490e09ff388f8b27c6bd5873d244c7cdbea4fd --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt @@ -0,0 +1,13 @@ +package org.briarproject.mailbox.core.settings + +import dagger.Module +import dagger.Provides +import org.briarproject.mailbox.core.db.Database + +@Module +class SettingsModule { + @Provides + fun provideSettingsManager(db: Database): SettingsManager { + return SettingsManagerImpl(db) + } +} diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt index b41ac5c317e00b69e523b35112fb64ca5ee01b89..2a9abd05bca1b569c945e5ae0cc888f6bef197ec 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt @@ -1,7 +1,9 @@ package org.briarproject.mailbox.core import dagger.Component +import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.settings.SettingsManager import javax.inject.Singleton @Singleton @@ -13,4 +15,6 @@ import javax.inject.Singleton interface TestComponent { fun injectCoreEagerSingletons(): CoreEagerSingletons fun getLifecycleManager(): LifecycleManager + fun getSettingsManager(): SettingsManager + fun getDatabase(): Database } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt index 38d46db1e289328a5f3f4fe3b4394327f2d043e0..20b9590c42713f4fc9ec144fbc436cdd995ec4db 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt @@ -8,6 +8,7 @@ import org.briarproject.mailbox.core.db.DatabaseConfig import org.briarproject.mailbox.core.db.DatabaseModule import org.briarproject.mailbox.core.lifecycle.LifecycleModule import org.briarproject.mailbox.core.server.WebServerModule +import org.briarproject.mailbox.core.settings.SettingsModule import org.briarproject.mailbox.core.system.Clock import java.io.File import javax.inject.Singleton @@ -17,6 +18,7 @@ import javax.inject.Singleton LifecycleModule::class, DatabaseModule::class, WebServerModule::class, + SettingsModule::class, // no Tor module ] ) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt index f2e0018ab7b6a39312c5fc3b8d001a4f801d1bce..b711dcb000d1a6138ed7bf1cd3609a2963131d9c 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt @@ -2,11 +2,11 @@ package org.briarproject.mailbox.core.db import org.briarproject.mailbox.core.TestUtils.deleteTestDirectory import org.briarproject.mailbox.core.api.Contact +import org.briarproject.mailbox.core.settings.Settings import org.briarproject.mailbox.core.system.Clock import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File -import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.assertNull @@ -23,8 +23,8 @@ abstract class JdbcDatabaseTest { @Throws(java.lang.Exception::class) fun open( resume: Boolean, - ): Database<Connection> { - val db: Database<Connection> = createDatabase( + ): Database { + val db: Database = createDatabase( TestDatabaseConfig(testDir) ) { System.currentTimeMillis() } if (!resume) deleteTestDirectory(testDir) @@ -36,9 +36,6 @@ abstract class JdbcDatabaseTest { @Throws(Exception::class) open fun testPersistence() { // Store some records - var db: Database<Connection> = open(false) - var txn = db.startTransaction() - val contact1 = Contact( 1, "4291ad1d-897d-4db4-9de9-ea3f78c5262e", @@ -51,38 +48,60 @@ abstract class JdbcDatabaseTest { "7931fa7a-077e-403a-8487-63261027d6d2", "12a61ca3-af0a-41d1-acc1-a0f4625f6e42" ) + var db: Database = open(false) + db.transaction(false) { txn -> - db.addContact(txn, contact1) - db.addContact(txn, contact2) - - db.commitTransaction(txn) + db.addContact(txn, contact1) + db.addContact(txn, contact2) + } db.close() // Check that the records are still there db = open(true) - txn = db.startTransaction() - - val contact1Reloaded1 = db.getContact(txn, 1) - val contact2Reloaded1 = db.getContact(txn, 2) - assertEquals(contact1, contact1Reloaded1) - assertEquals(contact2, contact2Reloaded1) - - // Delete one of the records - db.removeContact(txn, 1) - - db.commitTransaction(txn) + db.transaction(false) { txn -> + val contact1Reloaded1 = db.getContact(txn, 1) + val contact2Reloaded1 = db.getContact(txn, 2) + assertEquals(contact1, contact1Reloaded1) + assertEquals(contact2, contact2Reloaded1) + + // Delete one of the records + db.removeContact(txn, 1) + } db.close() // Check that the record is gone db = open(true) - txn = db.startTransaction() - - val contact1Reloaded2 = db.getContact(txn, 1) - val contact2Reloaded2 = db.getContact(txn, 2) - assertNull(contact1Reloaded2) - assertEquals(contact2, contact2Reloaded2) + db.transaction(true) { txn -> + val contact1Reloaded2 = db.getContact(txn, 1) + val contact2Reloaded2 = db.getContact(txn, 2) + assertNull(contact1Reloaded2) + assertEquals(contact2, contact2Reloaded2) + } + db.close() + } - db.commitTransaction(txn) + @Test + @Throws(java.lang.Exception::class) + open fun testMergeSettings() { + val before = Settings() + before["foo"] = "bar" + before["baz"] = "bam" + val update = Settings() + update["baz"] = "qux" + val merged = Settings() + merged["foo"] = "bar" + merged["baz"] = "qux" + + var db: Database = open(false) + var txn = db.transaction(false) { txn -> + // store 'before' + db.mergeSettings(txn, before, "namespace") + assertEquals(before, db.getSettings(txn, "namespace")) + + // merge 'update' + db.mergeSettings(txn, update, "namespace") + assertEquals(merged, db.getSettings(txn, "namespace")) + } db.close() } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b2bfb542ec32e88eb02307334032e797eb4358c --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/SettingsManagerTest.kt @@ -0,0 +1,49 @@ +package org.briarproject.mailbox.core.settings + +import org.briarproject.mailbox.core.DaggerTestComponent +import org.briarproject.mailbox.core.TestComponent +import org.briarproject.mailbox.core.TestModule +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import org.junit.jupiter.api.io.TempDir +import java.io.File +import kotlin.test.assertEquals + +@TestInstance(Lifecycle.PER_CLASS) +class SettingsManagerTest { + + private lateinit var testComponent: TestComponent + + @BeforeAll + fun setUp(@TempDir tempDir: File) { + testComponent = DaggerTestComponent.builder().testModule(TestModule(tempDir)).build() + testComponent.injectCoreEagerSingletons() + testComponent.getDatabase().open(null) + } + + @Test + @Throws(java.lang.Exception::class) + open fun testMergeSettings() { + val before = Settings() + before["foo"] = "bar" + before["baz"] = "bam" + val update = Settings() + update["baz"] = "qux" + val merged = Settings() + merged["foo"] = "bar" + merged["baz"] = "qux" + + val sm: SettingsManager = testComponent.getSettingsManager() + + // store 'before' + sm.mergeSettings(before, "namespace") + assertEquals(before, sm.getSettings("namespace")) + + // merge 'update' + sm.mergeSettings(update, "namespace") + assertEquals(merged, sm.getSettings("namespace")) + } + +}