Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import to.bitkit.di.IoDispatcher
import to.bitkit.di.json
import to.bitkit.ext.formatPlural
import to.bitkit.ext.nowMillis
import to.bitkit.ext.runSuspendCatching
import to.bitkit.models.ActivityBackupV1
import to.bitkit.models.BackupCategory
import to.bitkit.models.BackupItemStatus
Expand Down Expand Up @@ -424,7 +425,7 @@ class BackupRepo @Inject constructor(
}
}

suspend fun triggerBackup(category: BackupCategory) = withContext(ioDispatcher) {
suspend fun triggerBackup(category: BackupCategory): Result<Unit> = withContext(ioDispatcher) {
Comment thread
ben-kaufman marked this conversation as resolved.
Logger.debug("Backup starting for: '$category'", context = TAG)

val backupRequired = currentTimeMillis()
Expand All @@ -434,7 +435,13 @@ class BackupRepo @Inject constructor(
it.copy(running = true, required = backupRequired)
}

vssBackupClient.putObject(key = category.name, data = getBackupDataBytes(category))
val data = runSuspendCatching { getBackupDataBytes(category) }
.getOrElse {
markBackupFailed(category, backupRequired, it)
return@withContext Result.failure(it)
}

vssBackupClient.putObject(key = category.name, data = data)
.onSuccess {
runningBackups -= category
failedBackupRequired -= category
Expand All @@ -446,18 +453,21 @@ class BackupRepo @Inject constructor(
}
Logger.info("Backup succeeded for: '$category'", context = TAG)
}
.onFailure { e ->
runningBackups -= category
cacheStore.updateBackupStatus(category) {
if (it.required == backupRequired) {
failedBackupRequired[category] = backupRequired
} else {
failedBackupRequired -= category
}
it.copy(running = false)
}
Logger.error("Backup failed for: '$category'", e, context = TAG)
.onFailure { markBackupFailed(category, backupRequired, it) }
.map {}
}

private suspend fun markBackupFailed(category: BackupCategory, backupRequired: Long, e: Throwable) {
runningBackups -= category
cacheStore.updateBackupStatus(category) {
if (it.required == backupRequired) {
failedBackupRequired[category] = backupRequired
} else {
failedBackupRequired -= category
}
it.copy(running = false)
}
Logger.error("Backup failed for: '$category'", e, context = TAG)
}

private suspend fun getBackupDataBytes(category: BackupCategory): ByteArray = when (category) {
Expand Down Expand Up @@ -533,16 +543,8 @@ class BackupRepo @Inject constructor(

private suspend fun getWalletBackupDataBytes(): ByteArray {
val transfers = db.transferDao().getAll()
val privateReservations = privatePaykitAddressReservationRepo.get().backupSnapshot()
.onFailure {
Logger.warn("Failed to snapshot private Paykit reservations", it, context = TAG)
}
.getOrThrow()
val paykitSdkBackupState = privatePaykitRepo.get().backupSnapshot()
.onFailure {
Logger.warn("Failed to snapshot Paykit SDK state", it, context = TAG)
}
.getOrThrow()
val privateReservations = privatePaykitAddressReservationRepo.get().backupSnapshot().getOrThrow()
val paykitSdkBackupState = privatePaykitRepo.get().backupSnapshot().getOrThrow()

val payload = WalletBackupV1(
createdAt = currentTimeMillis(),
Expand Down
41 changes: 41 additions & 0 deletions app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import to.bitkit.services.PaykitSdkService
import to.bitkit.test.BaseUnitTest
import to.bitkit.utils.AppError
import javax.inject.Provider
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
Expand Down Expand Up @@ -111,6 +112,46 @@ class BackupRepoTest : BaseUnitTest() {
verify(settingsStore, never()).update(any())
}

@Test
fun `automatic wallet backup marks failure when Paykit snapshot fails`() = test {
whenever(clock.now()).thenReturn(Instant.fromEpochMilliseconds(3_000))
whenever { privatePaykitRepo.backupSnapshot() }
.thenReturn(Result.failure(BackupRepoTestError("paykit session missing capabilities")))
val backupStatuses = MutableStateFlow(
mapOf(
BackupCategory.WALLET to BackupItemStatus(
synced = 1_000,
required = 2_000,
),
)
)
val allowWalletClear = CompletableDeferred<Unit>().apply { complete(Unit) }
stubBackupStatuses(backupStatuses, allowWalletClear) {}
stubBackupObservers()

try {
sut.startObservingBackups()
Comment thread
jvsena42 marked this conversation as resolved.
runCurrent()
advanceTimeBy(5_000)
runCurrent()

verify(privatePaykitRepo).backupSnapshot()
verify(vssBackupClient, never()).putObject(eq(BackupCategory.WALLET.name), any())
val status = backupStatuses.value.getValue(BackupCategory.WALLET)
assertTrue(status.isRequired)
assertFalse(status.running)

advanceTimeBy(5_000)
runCurrent()

// still one attempt: the failed timestamp suppresses a retry until data changes again
verify(privatePaykitRepo, times(1)).backupSnapshot()
verify(vssBackupClient, never()).putObject(eq(BackupCategory.WALLET.name), any())
} finally {
sut.stopObservingBackups()
}
}

@Test
fun `start observing backs up stale required status after clearing running flag`() = test {
val backupStatuses = MutableStateFlow(
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1092.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the wallet backup failing repeatedly when Paykit state could not be read.
Loading