Prevent infinite archive attachment reconciliation attempts after server storage quota disagreement.

This commit is contained in:
jeffrey-signal
2025-09-29 14:24:19 -04:00
committed by Michelle Tang
parent 415021eedf
commit a37209d8ba
3 changed files with 37 additions and 4 deletions

View File

@@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.backup.MediaId
import java.lang.RuntimeException
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
@@ -66,6 +65,20 @@ class ArchiveAttachmentReconciliationJob private constructor(
private const val CDN_FETCH_LIMIT = 10_000
private const val DELETE_BATCH_SIZE = 10_000
/**
* Enqueues a reconciliation job if the retry limit hasn't been exceeded.
*
* @param forced If true, forces the job run to bypass any sync interval constraints.
*/
fun enqueueIfRetryAllowed(forced: Boolean) {
if (SignalStore.backup.archiveAttachmentReconciliationAttempts < 3) {
SignalStore.backup.archiveAttachmentReconciliationAttempts++
AppDependencies.jobManager.add(ArchiveAttachmentReconciliationJob(forced = forced))
} else {
Log.i(TAG, "Skip enqueueing reconciliation job: attempt limit exceeded.")
}
}
}
constructor(forced: Boolean = false) : this(
@@ -327,6 +340,7 @@ class ArchiveAttachmentReconciliationJob private constructor(
return null to Result.failure()
}
}
is NetworkResult.ApplicationError -> {
Log.w(TAG, "Failed to list remote media objects due to a crash.", result.getCause())
return null to Result.fatalFailure(RuntimeException(result.getCause()))

View File

@@ -173,7 +173,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
Log.i(TAG, "[$attachmentId] Remote storage is full, but our local state indicates that once we reconcile our storage, we should have enough. Enqueuing the reconciliation job and retrying.")
SignalStore.backup.remoteStorageGarbageCollectionPending = true
AppDependencies.jobManager.add(ArchiveAttachmentReconciliationJob(forced = true))
ArchiveAttachmentReconciliationJob.enqueueIfRetryAllowed(forced = true)
Result.retry(defaultBackoff())
}
@@ -206,6 +206,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
}
ArchiveUploadProgress.onAttachmentFinished(attachmentId)
SignalStore.backup.archiveAttachmentReconciliationAttempts = 0
}
return result
@@ -213,7 +214,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
private fun getServerQuota(): ByteSize? {
return runBlocking {
BackupRepository.getPaidType().successOrThrow()?.storageAllowanceBytes?.bytes
BackupRepository.getPaidType().successOrThrow().storageAllowanceBytes?.bytes
}
}

View File

@@ -82,6 +82,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded"
private const val KEY_BACKUP_DELETION_STATE = "backup.deletion.state"
private const val KEY_REMOTE_STORAGE_GARBAGE_COLLECTION_PENDING = "backup.remoteStorageGarbageCollectionPending"
private const val KEY_ARCHIVE_ATTACHMENT_RECONCILIATION_ATTEMPTS = "backup.archiveAttachmentReconciliationAttempts"
private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey"
@@ -393,10 +394,25 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var remoteStorageGarbageCollectionPending
get() = store.getBoolean(KEY_REMOTE_STORAGE_GARBAGE_COLLECTION_PENDING, false)
set(value) {
store.beginWrite().putBoolean(KEY_REMOTE_STORAGE_GARBAGE_COLLECTION_PENDING, value)
store.beginWrite()
.putBoolean(KEY_REMOTE_STORAGE_GARBAGE_COLLECTION_PENDING, value)
.apply()
NoRemoteArchiveGarbageCollectionPendingConstraint.Observer.notifyListeners()
}
/**
* Tracks archive attachment reconciliation attempts to prevent infinite retries when we disagree with the server about available storage space.
*/
var archiveAttachmentReconciliationAttempts: Int
get() {
return store.getInteger(KEY_ARCHIVE_ATTACHMENT_RECONCILIATION_ATTEMPTS, 0)
}
set(value) {
store.beginWrite()
.putInteger(KEY_ARCHIVE_ATTACHMENT_RECONCILIATION_ATTEMPTS, value)
.apply()
}
/**
* When we are told by the server that we are out of storage space, we should show
* UX treatment to make the user aware of this.
@@ -416,6 +432,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, false)
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET, false)
.apply()
archiveAttachmentReconciliationAttempts = 0
}
/**