Skip to content

Commit e699c54

Browse files
committed
fix: preserve backup times & fix race condition
1 parent 42a48c7 commit e699c54

File tree

2 files changed

+60
-25
lines changed

2 files changed

+60
-25
lines changed

app/src/main/java/to/bitkit/repositories/BackupRepo.kt

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import kotlinx.datetime.Clock
2323
import to.bitkit.R
2424
import to.bitkit.data.AppDb
2525
import to.bitkit.data.CacheStore
26-
import to.bitkit.data.SettingsData
2726
import to.bitkit.data.SettingsStore
28-
import to.bitkit.data.WidgetsData
2927
import to.bitkit.data.WidgetsStore
3028
import to.bitkit.data.backup.VssBackupClient
3129
import to.bitkit.data.resetPin
@@ -48,6 +46,7 @@ import to.bitkit.services.LightningService
4846
import to.bitkit.ui.shared.toast.ToastEventBus
4947
import to.bitkit.utils.Logger
5048
import to.bitkit.utils.jsonLogOf
49+
import java.util.concurrent.ConcurrentHashMap
5150
import javax.inject.Inject
5251
import javax.inject.Singleton
5352

@@ -72,6 +71,9 @@ class BackupRepo @Inject constructor(
7271
private val statusObserverJobs = mutableListOf<Job>()
7372
private val dataListenerJobs = mutableListOf<Job>()
7473
private var periodicCheckJob: Job? = null
74+
75+
private val runningBackups = ConcurrentHashMap.newKeySet<BackupCategory>()
76+
7577
private var isObserving = false
7678
private var lastNotificationTime = 0L
7779

@@ -97,6 +99,22 @@ class BackupRepo @Inject constructor(
9799
Logger.debug("Start observing backup statuses and data store changes", context = TAG)
98100

99101
scope.launch { vssBackupClient.setup() }
102+
103+
scope.launch {
104+
BackupCategory.entries.forEach { category ->
105+
if (category !in runningBackups) {
106+
cacheStore.updateBackupStatus(category) { status ->
107+
if (status.running) {
108+
Logger.debug("Clearing stale running flag for: '$category'", context = TAG)
109+
status.copy(running = false)
110+
} else {
111+
status
112+
}
113+
}
114+
}
115+
}
116+
}
117+
100118
startBackupStatusObservers()
101119
startDataStoreListeners()
102120
startPeriodicBackupFailureCheck()
@@ -269,29 +287,40 @@ class BackupRepo @Inject constructor(
269287
}
270288

271289
private fun scheduleBackup(category: BackupCategory) {
272-
// Cancel existing backup job for this category
273290
backupJobs[category]?.cancel()
274291

275292
Logger.verbose("Scheduling backup for: '$category'", context = TAG)
276293

277294
backupJobs[category] = scope.launch {
278-
// Set running immediately to prevent UI showing failure during debounce
295+
runningBackups += category
279296
cacheStore.updateBackupStatus(category) {
280297
it.copy(running = true)
281298
}
282299

283300
delay(BACKUP_DEBOUNCE)
284301

285-
// Double-check if backup is still needed
286302
val status = cacheStore.backupStatuses.first()[category] ?: BackupItemStatus()
287-
if (status.shouldBackup()) {
303+
if (status.isRequired && !shouldSkipBackup()) {
288304
triggerBackup(category)
289305
} else {
290-
// Backup no longer needed, reset running flag
306+
Logger.debug("Backup no longer needed for: '$category'", context = TAG)
307+
runningBackups -= category
291308
cacheStore.updateBackupStatus(category) {
292309
it.copy(running = false)
293310
}
294311
}
312+
}.also { job ->
313+
job.invokeOnCompletion { exception ->
314+
if (exception != null) {
315+
Logger.debug("Backup job cancelled for: '$category'", context = TAG)
316+
scope.launch {
317+
runningBackups -= category
318+
cacheStore.updateBackupStatus(category) {
319+
it.copy(running = false)
320+
}
321+
}
322+
}
323+
}
295324
}
296325
}
297326

@@ -335,12 +364,14 @@ class BackupRepo @Inject constructor(
335364
suspend fun triggerBackup(category: BackupCategory) = withContext(ioDispatcher) {
336365
Logger.debug("Backup starting for: '$category'", context = TAG)
337366

367+
runningBackups += category
338368
cacheStore.updateBackupStatus(category) {
339369
it.copy(running = true, required = currentTimeMillis())
340370
}
341371

342372
vssBackupClient.putObject(key = category.name, data = getBackupDataBytes(category))
343373
.onSuccess {
374+
runningBackups -= category
344375
cacheStore.updateBackupStatus(category) {
345376
it.copy(
346377
running = false,
@@ -350,6 +381,7 @@ class BackupRepo @Inject constructor(
350381
Logger.info("Backup succeeded for: '$category'", context = TAG)
351382
}
352383
.onFailure { e ->
384+
runningBackups -= category
353385
cacheStore.updateBackupStatus(category) {
354386
it.copy(running = false)
355387
}
@@ -452,34 +484,34 @@ class BackupRepo @Inject constructor(
452484
val tagMetadata = parsed.tagMetadata.map { it.toTagMetadataEntity() }
453485
db.tagMetadataDao().upsert(tagMetadata)
454486
Logger.debug("Restored ${tagMetadata.size} pre-activity metadata", TAG)
487+
parsed.createdAt
455488
}
456489

457490
performRestore(BackupCategory.SETTINGS) { dataBytes ->
458-
val parsed = json.decodeFromString<SettingsData>(String(dataBytes)).resetPin()
459-
settingsStore.update { parsed }
460491
val parsed = json.decodeFromString<SettingsBackupV1>(String(dataBytes))
461492
settingsStore.restoreFromBackup(parsed)
493+
parsed.createdAt
462494
}
463495
performRestore(BackupCategory.WIDGETS) { dataBytes ->
464-
val parsed = json.decodeFromString<WidgetsData>(String(dataBytes))
465-
widgetsStore.update { parsed }
466496
val parsed = json.decodeFromString<WidgetsBackupV1>(String(dataBytes))
467497
widgetsStore.restoreFromBackup(parsed)
498+
parsed.createdAt
468499
}
469500
performRestore(BackupCategory.WALLET) { dataBytes ->
470501
val parsed = json.decodeFromString<WalletBackupV1>(String(dataBytes))
471502
db.transferDao().upsert(parsed.transfers)
472503
Logger.debug("Restored ${parsed.transfers.size} transfers", context = TAG)
504+
parsed.createdAt
473505
}
474506
performRestore(BackupCategory.BLOCKTANK) { dataBytes ->
475507
val parsed = json.decodeFromString<BlocktankBackupV1>(String(dataBytes))
476-
blocktankRepo.restoreFromBackup(parsed).onSuccess {
477-
Logger.debug("Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs", TAG)
478-
}
508+
blocktankRepo.restoreFromBackup(parsed)
509+
parsed.createdAt
479510
}
480511
performRestore(BackupCategory.ACTIVITY) { dataBytes ->
481512
val parsed = json.decodeFromString<ActivityBackupV1>(String(dataBytes))
482513
activityRepo.restoreFromBackup(parsed)
514+
parsed.createdAt
483515
}
484516

485517
Logger.info("Full restore success", context = TAG)
@@ -503,24 +535,25 @@ class BackupRepo @Inject constructor(
503535

504536
private suspend fun performRestore(
505537
category: BackupCategory,
506-
restoreAction: suspend (ByteArray) -> Unit,
538+
restoreAction: suspend (dataBytes: ByteArray) -> Long,
507539
): Result<Unit> = runCatching {
540+
var createdAtTimestamp = currentTimeMillis()
541+
508542
vssBackupClient.getObject(category.name).map { it?.value }
509543
.onSuccess { dataBytes ->
510544
if (dataBytes == null) {
511545
Logger.warn("Restore null for: '$category'", context = TAG)
512546
} else {
513-
restoreAction(dataBytes)
547+
createdAtTimestamp = restoreAction(dataBytes)
514548
Logger.info("Restore success for: '$category'", context = TAG)
515549
}
516550
}
517551
.onFailure {
518552
Logger.debug("Restore error for: '$category'", context = TAG)
519553
}
520554

521-
val now = currentTimeMillis()
522555
cacheStore.updateBackupStatus(category) {
523-
it.copy(running = false, synced = now, required = now)
556+
it.copy(running = false, synced = createdAtTimestamp, required = createdAtTimestamp)
524557
}
525558
}
526559

app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -376,11 +376,11 @@ class BlocktankRepo @Inject constructor(
376376
Logger.debug("Blocktank state reset", context = TAG)
377377
}
378378

379-
suspend fun restoreFromBackup(backup: BlocktankBackupV1): Result<Unit> = withContext(bgDispatcher) {
379+
suspend fun restoreFromBackup(payload: BlocktankBackupV1): Result<Unit> = withContext(bgDispatcher) {
380380
return@withContext runCatching {
381-
coreService.blocktank.upsertOrderList(backup.orders)
382-
coreService.blocktank.upsertCjitList(backup.cjitEntries)
383-
backup.info?.let { info ->
381+
coreService.blocktank.upsertOrderList(payload.orders)
382+
coreService.blocktank.upsertCjitList(payload.cjitEntries)
383+
payload.info?.let { info ->
384384
coreService.blocktank.setInfo(info)
385385
}
386386

@@ -389,11 +389,13 @@ class BlocktankRepo @Inject constructor(
389389

390390
_blocktankState.update {
391391
it.copy(
392-
orders = backup.orders,
393-
cjitEntries = backup.cjitEntries,
394-
info = backup.info,
392+
orders = payload.orders,
393+
cjitEntries = payload.cjitEntries,
394+
info = payload.info,
395395
)
396396
}
397+
}.onSuccess {
398+
Logger.debug("Restored ${payload.orders.size} orders, ${payload.cjitEntries.size} CJITs", TAG)
397399
}
398400
}
399401

0 commit comments

Comments
 (0)