Respect 429 in SVR write requests.

This commit is contained in:
Greyson Parrelli
2026-01-07 12:23:55 -05:00
committed by jeffrey-signal
parent e3b569ca5b
commit 4b41989b30
7 changed files with 39 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.Job.Result
import org.thoughtcrime.securesms.jobmanager.JsonJobData import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -20,6 +21,7 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSes
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Attempts to reset the guess on the SVR PIN. Intended to be enqueued after a successful restore. * Attempts to reset the guess on the SVR PIN. Intended to be enqueued after a successful restore.
@@ -163,6 +165,11 @@ class ResetSvrGuessCountJob private constructor(
Log.w(TAG, "Failed to expose the backup. Giving up. $svr") Log.w(TAG, "Failed to expose the backup. Giving up. $svr")
Result.success() Result.success()
} }
is BackupResponse.RateLimited -> {
val backoff = response.retryAfter ?: defaultBackoff().milliseconds
Log.w(TAG, "Hit rate limit. Retrying in $backoff")
Result.retry(backoff.inWholeMilliseconds)
}
is BackupResponse.NetworkError -> { is BackupResponse.NetworkError -> {
Log.w(TAG, "Hit a network error. Retrying. $svr", response.exception) Log.w(TAG, "Hit a network error. Retrying. $svr", response.exception)
Result.retry(defaultBackoff()) Result.retry(defaultBackoff())

View File

@@ -19,6 +19,7 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSes
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2 import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Ensures a user's SVR data is written to SVR2. * Ensures a user's SVR data is written to SVR2.
@@ -109,6 +110,11 @@ class Svr2MirrorJob private constructor(parameters: Parameters, private var seri
Log.w(TAG, "Failed to expose the backup. Giving up.") Log.w(TAG, "Failed to expose the backup. Giving up.")
Result.success() Result.success()
} }
is BackupResponse.RateLimited -> {
val backoff = response.retryAfter ?: defaultBackoff().milliseconds
Log.w(TAG, "Hit rate limit. Retrying in $backoff")
Result.retry(backoff.inWholeMilliseconds)
}
is BackupResponse.NetworkError -> { is BackupResponse.NetworkError -> {
Log.w(TAG, "Hit a network error. Retrying.", response.exception) Log.w(TAG, "Hit a network error. Retrying.", response.exception)
Result.retry(defaultBackoff()) Result.retry(defaultBackoff())

View File

@@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSes
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3 import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Ensures a user's SVR data is written to SVR3. * Ensures a user's SVR data is written to SVR3.
@@ -93,6 +94,11 @@ class Svr3MirrorJob private constructor(parameters: Parameters, private var seri
Result.retry(defaultBackoff()) Result.retry(defaultBackoff())
} }
} }
is BackupResponse.RateLimited -> {
val backoff = response.retryAfter ?: defaultBackoff().milliseconds
Log.w(TAG, "Hit rate limit. Retrying in $backoff")
Result.retry(backoff.inWholeMilliseconds)
}
BackupResponse.EnclaveNotFound -> { BackupResponse.EnclaveNotFound -> {
Log.w(TAG, "Could not find the enclave. Giving up.") Log.w(TAG, "Could not find the enclave. Giving up.")
Result.success() Result.success()

View File

@@ -257,6 +257,7 @@ object SvrRepository {
BackupResponse.ExposeFailure -> it BackupResponse.ExposeFailure -> it
is BackupResponse.NetworkError -> it is BackupResponse.NetworkError -> it
BackupResponse.ServerRejected -> it BackupResponse.ServerRejected -> it
is BackupResponse.RateLimited -> it
BackupResponse.EnclaveNotFound -> null BackupResponse.EnclaveNotFound -> null
is BackupResponse.Success -> null is BackupResponse.Success -> null
} }

View File

@@ -23,6 +23,7 @@ import org.signal.registration.NetworkController.RegisterAccountError
import org.signal.registration.NetworkController.RegisterAccountResponse import org.signal.registration.NetworkController.RegisterAccountResponse
import org.signal.registration.NetworkController.RegistrationLockResponse import org.signal.registration.NetworkController.RegistrationLockResponse
import org.signal.registration.NetworkController.RegistrationNetworkResult import org.signal.registration.NetworkController.RegistrationNetworkResult
import org.signal.registration.NetworkController.RegistrationNetworkResult.*
import org.signal.registration.NetworkController.RequestVerificationCodeError import org.signal.registration.NetworkController.RequestVerificationCodeError
import org.signal.registration.NetworkController.SessionMetadata import org.signal.registration.NetworkController.SessionMetadata
import org.signal.registration.NetworkController.SubmitVerificationCodeError import org.signal.registration.NetworkController.SubmitVerificationCodeError
@@ -465,27 +466,30 @@ class RealNetworkController(
when (response) { when (response) {
is BackupResponse.Success -> { is BackupResponse.Success -> {
Log.i(TAG, "[backupMasterKeyToSvr] Successfully backed up master key to SVR2") Log.i(TAG, "[backupMasterKeyToSvr] Successfully backed up master key to SVR2")
RegistrationNetworkResult.Success(Unit) Success(Unit)
} }
is BackupResponse.ApplicationError -> { is BackupResponse.ApplicationError -> {
Log.w(TAG, "[backupMasterKeyToSvr] Application error", response.exception) Log.w(TAG, "[backupMasterKeyToSvr] Application error", response.exception)
RegistrationNetworkResult.ApplicationError(response.exception) ApplicationError(response.exception)
} }
is BackupResponse.NetworkError -> { is BackupResponse.NetworkError -> {
Log.w(TAG, "[backupMasterKeyToSvr] Network error", response.exception) Log.w(TAG, "[backupMasterKeyToSvr] Network error", response.exception)
RegistrationNetworkResult.NetworkError(response.exception) NetworkError(response.exception)
} }
is BackupResponse.EnclaveNotFound -> { is BackupResponse.EnclaveNotFound -> {
Log.w(TAG, "[backupMasterKeyToSvr] Enclave not found") Log.w(TAG, "[backupMasterKeyToSvr] Enclave not found")
RegistrationNetworkResult.Failure(NetworkController.BackupMasterKeyError.EnclaveNotFound) Failure(NetworkController.BackupMasterKeyError.EnclaveNotFound)
} }
is BackupResponse.ExposeFailure -> { is BackupResponse.ExposeFailure -> {
Log.w(TAG, "[backupMasterKeyToSvr] Expose failure -- per spec, treat as success.") Log.w(TAG, "[backupMasterKeyToSvr] Expose failure -- per spec, treat as success.")
RegistrationNetworkResult.Success(Unit) Success(Unit)
} }
is BackupResponse.ServerRejected -> { is BackupResponse.ServerRejected -> {
Log.w(TAG, "[backupMasterKeyToSvr] Server rejected") Log.w(TAG, "[backupMasterKeyToSvr] Server rejected")
RegistrationNetworkResult.NetworkError(IOException("Server rejected backup request")) NetworkError(IOException("Server rejected backup request"))
}
is BackupResponse.RateLimited -> {
NetworkError(IOException("Rate limited"))
} }
} }
} catch (e: IOException) { } catch (e: IOException) {

View File

@@ -9,6 +9,7 @@ import org.signal.core.models.MasterKey
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import java.io.IOException import java.io.IOException
import kotlin.jvm.Throws import kotlin.jvm.Throws
import kotlin.time.Duration
interface SecureValueRecovery { interface SecureValueRecovery {
@@ -88,6 +89,9 @@ interface SecureValueRecovery {
/** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */ /** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */
data class NetworkError(val exception: IOException) : BackupResponse() data class NetworkError(val exception: IOException) : BackupResponse()
/** The client request was rate-limited. */
data class RateLimited(val retryAfter: Duration?) : BackupResponse()
/** Something went wrong when making the request that is related to application logic. */ /** Something went wrong when making the request that is related to application logic. */
data class ApplicationError(val exception: Throwable) : BackupResponse() data class ApplicationError(val exception: Throwable) : BackupResponse()
} }

View File

@@ -30,6 +30,7 @@ import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.util.JsonUtil import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.io.IOException import java.io.IOException
import kotlin.time.Duration.Companion.seconds
import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse
import org.signal.svr2.proto.ExposeResponse as ProtoExposeResponse import org.signal.svr2.proto.ExposeResponse as ProtoExposeResponse
import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse
@@ -220,10 +221,10 @@ class SecureValueRecoveryV2(
} }
} catch (e: NonSuccessfulResponseCodeException) { } catch (e: NonSuccessfulResponseCodeException) {
Log.w(TAG, "[Set] Failed with a non-successful response code exception!", e) Log.w(TAG, "[Set] Failed with a non-successful response code exception!", e)
if (e.code == 404) { when (e.code) {
BackupResponse.EnclaveNotFound 404 -> BackupResponse.EnclaveNotFound
} else { 429 -> BackupResponse.RateLimited(e.headers["retry-after"]?.toLongOrNull()?.seconds)
BackupResponse.ApplicationError(e) else -> BackupResponse.ApplicationError(e)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, "[Set] Failed with a network exception!", e) Log.w(TAG, "[Set] Failed with a network exception!", e)