[#1151] Background syncing

* [#1151] Background syncing

- A periodic background block synchronization has been added. When the device is connected to the internet using an unmetered connection and is plugged into the power, the background task will start to synchronize blocks randomly between 3 and 4 a.m.
- The background worker was in place but not fully working, plus was set to trigger randomly in 24 hours
- Changelog update
- Closes #1151
- Closes #634
- Its follow-up #1249
- Its follow-up #1258
This commit is contained in:
Honza Rychnovský 2024-02-16 22:00:18 +01:00 committed by GitHub
parent 79de1690bb
commit bf618a1ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 94 additions and 13 deletions

View File

@ -9,6 +9,11 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
### Added
- A periodic background block synchronization has been added. When the device is connected to the internet using an
unmetered connection and is plugged into the power, the background task will start to synchronize blocks randomly
between 3 and 4 a.m.
## [0.2.0 (554)] - 2024-02-13
### Changed

View File

@ -23,4 +23,5 @@
# kotlinx.datetime supports kotlinx.serialization, but we don't use kotlinx.serialization elsewhere
# in the projects, so the classes aren't present. These warnings are safe to suppress.
-dontwarn kotlinx.serialization.KSerializer
-dontwarn kotlinx.serialization.Serializable
-dontwarn kotlinx.serialization.Serializable
-dontwarn kotlinx.serialization.internal.AbstractPolymorphicSerializer

View File

@ -12,55 +12,117 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.global.getInstance
import co.electriccoin.zcash.spackle.Twig
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.until
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlin.time.toJavaDuration
// TODO [#1249]: Add documentation and tests on background syncing
// TODO [#1249]: https://github.com/Electric-Coin-Company/zashi-android/issues/1249
@Keep
class SyncWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun doWork(): Result {
// Enhancements to this implementation would be:
// - Quit early if the synchronizer is null after a timeout period
// - Return better status information
Twig.debug { "BG Sync: starting..." }
WalletCoordinator.getInstance(applicationContext).synchronizer
.flatMapLatest {
Twig.debug { "BG Sync: synchronizer: $it" }
it?.status?.combine(it.progress) { status, progress ->
StatusAndProgress(status, progress)
StatusAndProgress(status, progress).also {
Twig.debug { "BG Sync: result: $it" }
}
} ?: emptyFlow()
}
.takeWhile {
it.status != Synchronizer.Status.DISCONNECTED && it.progress.isLessThanHundredPercent()
it.status != Synchronizer.Status.DISCONNECTED &&
it.status != Synchronizer.Status.SYNCED
}
.collect()
Twig.debug { "BG Sync: terminating..." }
return Result.success()
}
companion object {
/*
* There may be better periods; we have not optimized for this yet.
*/
private val DEFAULT_SYNC_PERIOD = 24.hours
private val SYNC_PERIOD = 24.hours
private val SYNC_DAY_SHIFT = 1.days // Move to tomorrow
private val SYNC_START_TIME_HOURS = 3.hours // Start around 3 a.m. at night
private val SYNC_START_TIME_MINUTES = 60.minutes // Randomize with minutes until 4 a.m.
fun newWorkRequest(): PeriodicWorkRequest {
val targetTimeDiff = calculateTargetTimeDifference()
Twig.debug { "BG Sync: necessary trigger delay time: $targetTimeDiff" }
val constraints =
Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
return PeriodicWorkRequestBuilder<SyncWorker>(DEFAULT_SYNC_PERIOD.toJavaDuration())
// TODO [#1258]: Consider using flexInterval in BG sync trigger planning
// TODO [#1258]: https://github.com/Electric-Coin-Company/zashi-android/issues/1258
return PeriodicWorkRequestBuilder<SyncWorker>(SYNC_PERIOD.toJavaDuration())
.setConstraints(constraints)
.setInitialDelay(targetTimeDiff.toJavaDuration())
.build()
}
private fun calculateTargetTimeDifference(): Duration {
val currentTimeZone: TimeZone = TimeZone.currentSystemDefault()
val now: Instant = Clock.System.now()
val targetTime =
now
.plus(SYNC_DAY_SHIFT)
.toLocalDateTime(currentTimeZone)
.date
.atTime(
hour = SYNC_START_TIME_HOURS.inWholeHours.toInt(),
// Even though the WorkManager will trigger the work approximately at the set time, it's
// better to randomize time in 3-4 a.m. This generates a number between 0 (inclusive) and 60
// (exclusive)
minute = Random.nextInt(0, SYNC_START_TIME_MINUTES.inWholeMinutes.toInt())
)
Twig.debug { "BG Sync: calculated target time: ${targetTime.time}" }
return now.until(
other = targetTime.toInstant(currentTimeZone),
unit = DateTimeUnit.MILLISECOND,
timeZone = currentTimeZone
).toDuration(DurationUnit.MILLISECONDS)
}
}
}
private data class StatusAndProgress(val status: Synchronizer.Status, val progress: PercentDecimal)
// Enhancement to this implementation would be returning a better status information
private data class StatusAndProgress(
val status: Synchronizer.Status,
val progress: PercentDecimal
)

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.work
import android.content.Context
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.WorkManager
import co.electriccoin.zcash.spackle.Twig
object WorkIds {
const val WORK_ID_BACKGROUND_SYNC = "co.electriccoin.zcash.background_sync"
@ -10,11 +11,23 @@ object WorkIds {
fun enableBackgroundSynchronization(context: Context) {
val workManager = WorkManager.getInstance(context)
Twig.debug {
"BG Sync: existing work details:" +
" ${workManager.getWorkInfosForUniqueWork(WORK_ID_BACKGROUND_SYNC).get()}"
}
// Note: Re-enqueuing existing work is okay. Another approach would be to validate the existing work and
// enqueue it if it is not planned yet or not in a valid state
workManager.enqueueUniquePeriodicWork(
WORK_ID_BACKGROUND_SYNC,
ExistingPeriodicWorkPolicy.KEEP,
SyncWorker.newWorkRequest()
)
Twig.debug {
"BG Sync: newly enqueued work details:" +
" ${workManager.getWorkInfosForUniqueWork(WORK_ID_BACKGROUND_SYNC).get()}"
}
}
fun disableBackgroundSynchronization(context: Context) {