Respect server retry-after for challenged sends in IndividualSendJobV2.

This commit is contained in:
Greyson Parrelli
2026-09-17 11:43:44 -04:00
parent cb0338adbe
commit a3f900324f
3 changed files with 28 additions and 2 deletions
@@ -114,6 +114,11 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
)
}
@JvmStatic
fun getMessageId(serializedData: ByteArray?): Long {
return IndividualSendJobV2Data.ADAPTER.decode(serializedData!!).messageId
}
private fun logPrefix(sentTimestamp: Long? = null, messageId: Long): String = "[${sentTimestamp ?: "?"}][$messageId]"
}
@@ -256,6 +261,12 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
}
ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(recipient)
if (SignalStore.rateLimit.needsRecaptcha()) {
Log.i(TAG, "${logPrefix(message.sentTimeMillis)} Successfully sent message. Assuming reCAPTCHA no longer needed.")
SignalStore.rateLimit.onProofAccepted()
}
Log.i(TAG, "${logPrefix(message.sentTimeMillis)} Sent message.")
Result.success()
},
@@ -302,8 +313,14 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
when (ProofRequiredExceptionHandler.handle(context, proofException, threadRecipient, threadId, messageId)) {
ProofRequiredExceptionHandler.Result.RETRY_NOW -> Result.retry(0L)
ProofRequiredExceptionHandler.Result.RETRY_LATER,
ProofRequiredExceptionHandler.Result.RETHROW -> Result.retry(nextRunAttemptBackoff(runAttempt + 1))
ProofRequiredExceptionHandler.Result.RETRY_LATER -> Result.retry(nextRunAttemptBackoff(runAttempt + 1))
ProofRequiredExceptionHandler.Result.RETHROW -> {
val defaultBackoff = nextRunAttemptBackoff(runAttempt + 1)
val serverBackoff = error.retryAfter?.inWholeMilliseconds ?: 0L
val backoff = maxOf(defaultBackoff, serverBackoff)
Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Unresolved challenge, retryAfter=${error.retryAfter}, using backoff=${backoff}ms")
Result.retry(backoff)
}
}
}
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.IndividualSendJob;
import org.thoughtcrime.securesms.jobs.IndividualSendJobV2;
import java.util.Set;
@@ -37,6 +38,8 @@ public final class RateLimitUtil {
AppDependencies.getJobManager().update((job) -> {
if (job.getFactoryKey().equals(IndividualSendJob.KEY) && messageIds.contains(IndividualSendJob.getMessageId(job.getSerializedData()))) {
return job.withNextBackoffInterval(0);
} else if (job.getFactoryKey().equals(IndividualSendJobV2.KEY) && messageIds.contains(IndividualSendJobV2.getMessageId(job.getSerializedData()))) {
return job.withNextBackoffInterval(0);
} else if (job.getFactoryKey().equals(PushGroupSendJob.KEY) && messageIds.contains(PushGroupSendJob.getMessageId(job.getSerializedData()))) {
return job.withNextBackoffInterval(0);
} else {
@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.keyvalue.MiscellaneousValues
import org.thoughtcrime.securesms.keyvalue.RateLimitValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler
@@ -106,6 +107,7 @@ class IndividualSendJobV2Test {
private val recipientId: RecipientId = RecipientId.from(2L)
private lateinit var misc: MiscellaneousValues
private lateinit var rateLimit: RateLimitValues
private lateinit var recipient: Recipient
private lateinit var self: Recipient
@@ -129,6 +131,10 @@ class IndividualSendJobV2Test {
every { misc.isClientDeprecated } returns false
every { SignalStore.misc } returns misc
rateLimit = mockk(relaxUnitFun = true)
every { rateLimit.needsRecaptcha() } returns false
every { SignalStore.rateLimit } returns rateLimit
every { signalStore.account.aci } returns selfAci
every { signalStore.account.requireAci() } returns selfAci
every { signalStore.account.isMultiDevice } returns false