General cleanup.

Additional KDocs. Log cleanup. Cleaner close and shutdown behavior. Improved retry functions. New and updated constants. Database migrataions.
This commit is contained in:
Kevin Gorham 2020-02-21 18:22:04 -05:00
parent 52bb1d108d
commit 4b1cb76f42
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
11 changed files with 148 additions and 22 deletions

View File

@ -31,7 +31,7 @@ class Initializer(
}
/**
* The path this initializer will use when checking for and downloaading sapling params. This
* The path this initializer will use when checking for and downloading sapling params. This
* value is derived from the appContext when this class is constructed.
*/
private val pathParams: String = "${appContext.cacheDir.absolutePath}/params"
@ -130,6 +130,12 @@ class Initializer(
/**
* Loads the rust library and previously used birthday for use by all other components. This is
* the most common use case for the initializer--reopening a wallet that was previously created.
*
* @param birthday birthday height of the wallet. This value is passed to the
* [CompactBlockProcessor] and becomes a factor in determining the lower bounds height that this
* wallet will use. This height helps with determining where to start downloading as well as how
* far back to go during a rewind. Every wallet has a birthday and the initializer depends on
* this value but does not own it.
*/
fun open(birthday: WalletBirthday): Initializer {
twig("Opening wallet with birthday ${birthday.height}")
@ -204,7 +210,10 @@ class Initializer(
* derivation.
*/
private fun requireRustBackend(): RustBackend {
if (!isInitialized) rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
if (!isInitialized) {
twig("Initializing cache: $pathCacheDb data: $pathDataDb params: $pathParams")
rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
}
return rustBackend
}
@ -271,13 +280,14 @@ class Initializer(
val parentDir: String =
appContext.getDatabasePath("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
return File(parentDir, "${alias}_$dbFileName").absolutePath
val prefix = if (alias.endsWith('_')) alias else "${alias}_"
return File(parentDir, "$prefix$dbFileName").absolutePath
}
}
/**
* Model object for holding wallet birthdays. It is only used by this class.
* Model object for holding wallet birthday. It is only used by this class.
*/
data class WalletBirthday(
val height: Int = -1,

View File

@ -45,4 +45,8 @@ class CompactBlockDbStore(
override suspend fun rewindTo(height: Int) = withContext(IO) {
cacheDao.rewindTo(height)
}
override fun close() {
cacheDb.close()
}
}

View File

@ -37,5 +37,10 @@ open class CompactBlockDownloader(
compactBlockStore.getLatestHeight()
}
fun stop() {
lightwalletService.shutdown()
compactBlockStore.close()
}
}

View File

@ -23,4 +23,9 @@ interface CompactBlockStore {
* Meaning, if max height is 100 block and rewindTo(50) is called, then the highest block remaining will be 49.
*/
suspend fun rewindTo(height: Int)
/**
* Close any connections to the block store.
*/
fun close()
}

View File

@ -1,6 +1,8 @@
package cash.z.wallet.sdk.ext
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.roundToInt
import kotlin.math.roundToLong
internal typealias Leaf = String
@ -129,10 +131,10 @@ inline fun <R> Twig.twig(logMessage: String, block: () -> R): R {
* (otherwise the function and its "block" param would have to suspend)
*/
inline fun <R> Twig.twigTask(logMessage: String, block: () -> R): R {
twig("$logMessage - started | on thread ${Thread.currentThread().name})")
twig("$logMessage - started | on thread ${Thread.currentThread().name}")
val start = System.nanoTime()
val result = block()
val elapsed = ((System.nanoTime() - start)/1e6)
val elapsed = ((System.nanoTime() - start) / 1e5).roundToLong() / 10L
twig("$logMessage - completed | in $elapsed ms" +
" on thread ${Thread.currentThread().name}")
return result

View File

@ -6,22 +6,58 @@ import kotlinx.coroutines.delay
import java.io.File
import kotlin.random.Random
suspend inline fun retryUpTo(retries: Int, initialDelayMillis: Long = 10L, block: (Int) -> Unit) {
/**
* Execute the given block and if it fails, retry up to [retries] more times. If none of the
* retries succeed then throw the final error, which can be wrapped in order to add more context.
*
* @param retries the number of times to retry the block after the first attempt fails.
* @param exceptionWrapper a function that can wrap the final failure to add more useful information
* or context. Default behavior is to just return the final exception.
* @param initialDelayMillis the initial amount of time to wait before the first retry.
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
* exception is thrown up to [retries] attempts.
*/
suspend inline fun retryUpTo(retries: Int, exceptionWrapper: (Throwable) -> Throwable = { it }, initialDelayMillis: Long = 500L, block: (Int) -> Unit) {
var failedAttempts = 0
while (failedAttempts < retries) {
while (failedAttempts <= retries) {
try {
block(failedAttempts)
return
} catch (t: Throwable) {
failedAttempts++
if (failedAttempts >= retries) throw t
val duration = Math.pow(initialDelayMillis.toDouble(), failedAttempts.toDouble()).toLong()
twig("failed due to $t retrying (${failedAttempts + 1}/$retries) in ${duration}s...")
if (failedAttempts > retries) throw exceptionWrapper(t)
val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong()
twig("failed due to $t retrying (${failedAttempts}/$retries) in ${duration}s...")
delay(duration)
}
}
}
/**
* Execute the given block and if it fails, retry up to [retries] more times, using thread sleep
* instead of suspending. If none of the retries succeed then throw the final error. This function
* is intended to be called with no parameters, i.e., it is designed to use its defaults.
*
* @param retries the number of times to retry. Typically, this should be low.
* @param sleepTime the amount of time to sleep in between retries. Typically, this should be an
* amount of time that is hard to perceive.
* @param block the block of logic to try.
*/
inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) -> Unit) {
var failedAttempts = 0
while (failedAttempts <= retries) {
try {
block(failedAttempts)
return
} catch (t: Throwable) {
failedAttempts++
if (failedAttempts > retries) throw t
twig("failed due to $t simply retrying (${failedAttempts}/$retries) in ${sleepTime}ms...")
Thread.sleep(sleepTime)
}
}
}
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, block: () -> Unit) {
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
while (true) {
@ -35,13 +71,13 @@ suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Bo
}
sequence++
// I^(1/4)n + jitter
// initialDelay^(sequence/4) + jitter
var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble()/4.0)).toLong() + Random.nextLong(1000L)
if (duration > maxDelayMillis) {
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
sequence /= 2
}
twig("Failed due to $t retrying in ${duration}ms...")
twig("Failed due to $t backing off and retrying in ${duration}ms...")
delay(duration)
}
}

View File

@ -29,6 +29,11 @@ open class ZcashSdkCommon {
*/
val MAX_REORG_SIZE = 100
/**
* The maximum length of a memo.
*/
val MAX_MEMO_SIZE = 512
/**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
* by the rust backend but it is helpful to know what it is set to and should be kept in sync.
@ -81,7 +86,7 @@ open class ZcashSdkCommon {
val DB_DATA_NAME = "Data.db"
val DB_CACHE_NAME = "Cache.db"
open val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_"
open val DEFAULT_DB_NAME_PREFIX = "ZcashSdk"
/**
* File name for the sappling spend params

View File

@ -60,7 +60,6 @@ class FlowPagedListBuilder<Key, Value>(
}
do {
twig("zzzzz do this while...")
if (::dataSource.isInitialized) {
dataSource.removeInvalidatedCallback(callback)
}

View File

@ -4,16 +4,14 @@ import android.content.Context
import androidx.paging.PagedList
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import cash.z.wallet.sdk.db.BlockDao
import cash.z.wallet.sdk.db.DerivedDataDb
import cash.z.wallet.sdk.db.TransactionDao
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.entity.EncodedTransaction
import cash.z.wallet.sdk.entity.TransactionEntity
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.android.toFlowPagedList
import cash.z.wallet.sdk.ext.android.toRefreshable
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
@ -39,9 +37,14 @@ open class PagedTransactionRepository(
) : this(
Room.databaseBuilder(context, DerivedDataDb::class.java, dataDbName)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.addMigrations(MIGRATION_4_3)
.build(),
pageSize
)
init {
derivedDataDb.openHelper.writableDatabase.beginTransaction()
derivedDataDb.openHelper.writableDatabase.endTransaction()
}
private val blocks: BlockDao = derivedDataDb.blockDao()
private val transactions: TransactionDao = derivedDataDb.transactionDao()
@ -80,4 +83,61 @@ open class PagedTransactionRepository(
derivedDataDb.close()
}
}
//
// Migrations
//
companion object {
// val MIGRATION_3_4 = object : Migration(3, 4) {
// override fun migrate(database: SupportSQLiteDatabase) {
// database.execSQL("PRAGMA foreign_keys = OFF;")
// database.execSQL("""
// CREATE TABLE IF NOT EXISTS received_notes_new (
// id_note INTEGER PRIMARY KEY, tx INTEGER NOT NULL,
// output_index INTEGER NOT NULL, account INTEGER NOT NULL,
// diversifier BLOB NOT NULL, value INTEGER NOT NULL,
// rcm BLOB NOT NULL, nf BLOB NOT NULL UNIQUE,
// is_change INTEGER NOT NULL, memo BLOB,
// spent INTEGER
// ); """.trimIndent()
// )
// database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
// database.execSQL("DROP TABLE received_notes;")
// database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
// database.execSQL("PRAGMA foreign_keys = ON;")
// }
// }
val MIGRATION_4_3 = object : Migration(4, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("PRAGMA foreign_keys = OFF;")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS received_notes_new (
id_note INTEGER PRIMARY KEY,
tx INTEGER NOT NULL,
output_index INTEGER NOT NULL,
account INTEGER NOT NULL,
diversifier BLOB NOT NULL,
value INTEGER NOT NULL,
rcm BLOB NOT NULL,
nf BLOB NOT NULL UNIQUE,
is_change INTEGER NOT NULL,
memo BLOB,
spent INTEGER,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (account) REFERENCES accounts(account),
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
CONSTRAINT tx_output UNIQUE (tx, output_index)
); """.trimIndent()
)
database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
database.execSQL("DROP TABLE received_notes;")
database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
database.execSQL("PRAGMA foreign_keys = ON;")
}
}
}
}

View File

@ -24,6 +24,6 @@ object ZcashSdk : ZcashSdkCommon() {
*/
override val DEFAULT_LIGHTWALLETD_HOST = "lightd-main.zecwallet.co"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_mainnet_"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_mainnet"
}

View File

@ -24,6 +24,6 @@ object ZcashSdk : ZcashSdkCommon() {
*/
override val DEFAULT_LIGHTWALLETD_HOST = "lightd-test.zecwallet.co"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_testnet_"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_testnet"
}