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:
parent
52bb1d108d
commit
4b1cb76f42
|
@ -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,
|
||||
|
|
|
@ -45,4 +45,8 @@ class CompactBlockDbStore(
|
|||
override suspend fun rewindTo(height: Int) = withContext(IO) {
|
||||
cacheDao.rewindTo(height)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cacheDb.close()
|
||||
}
|
||||
}
|
|
@ -37,5 +37,10 @@ open class CompactBlockDownloader(
|
|||
compactBlockStore.getLatestHeight()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
lightwalletService.shutdown()
|
||||
compactBlockStore.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -60,7 +60,6 @@ class FlowPagedListBuilder<Key, Value>(
|
|||
}
|
||||
|
||||
do {
|
||||
twig("zzzzz do this while...")
|
||||
if (::dataSource.isInitialized) {
|
||||
dataSource.removeInvalidatedCallback(callback)
|
||||
}
|
||||
|
|
|
@ -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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue