Utilize re-entrant locking for in app payments instead of synchronized blocks.

This commit is contained in:
Alex Hart
2024-11-08 10:48:03 -04:00
committed by Greyson Parrelli
parent a79b4c3ba0
commit ed24fd0c4b
16 changed files with 61 additions and 25 deletions

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import java.util.Locale
import kotlin.concurrent.withLock
class InternalDonorErrorConfigurationViewModel : ViewModel() {
@@ -101,7 +102,7 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
fun save(): Completable {
val snapshot = store.state
val saveState = Completable.fromAction {
synchronized(InAppPaymentSubscriberRecord.Type.DONATION) {
InAppPaymentSubscriberRecord.Type.DONATION.lock.withLock {
when {
snapshot.selectedBadge?.isGift() == true -> handleGiftExpiration(snapshot)
snapshot.selectedBadge?.isBoost() == true -> handleBoostExpiration(snapshot)
@@ -116,7 +117,7 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
fun clearErrorState(): Completable {
return Completable.fromAction {
synchronized(InAppPaymentSubscriberRecord.Type.DONATION) {
InAppPaymentSubscriberRecord.Type.DONATION.lock.withLock {
SignalStore.inAppPayments.setExpiredBadge(null)
SignalStore.inAppPayments.setExpiredGiftBadge(null)
SignalStore.inAppPayments.unexpectedSubscriptionCancelationReason = null

View File

@@ -56,6 +56,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentPro
import java.security.SecureRandom
import java.util.Currency
import java.util.Optional
import java.util.concurrent.locks.Lock
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
@@ -231,10 +232,11 @@ object InAppPaymentsRepository {
/**
* Returns the object to utilize as a mutex for recurring subscriptions.
*/
fun resolveMutex(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Any {
@WorkerThread
fun resolveLock(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Lock {
val payment = SignalDatabase.inAppPayments.getById(inAppPaymentId) ?: error("Not found")
return payment.type.requireSubscriberType()
return payment.type.requireSubscriberType().lock
}
/**

View File

@@ -214,7 +214,7 @@ object RecurringInAppPaymentRepository {
subscriptionLevel,
subscriber.currency.currencyCode,
levelUpdateOperation.idempotencyKey.serialize(),
subscriberType
subscriberType.lock
)
}
.flatMapCompletable {

View File

@@ -9,6 +9,8 @@ import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
/**
* Represents a SubscriberId and metadata that can be used for a recurring
@@ -24,7 +26,7 @@ data class InAppPaymentSubscriberRecord(
/**
* Serves as the mutex by which to perform mutations to subscriptions.
*/
enum class Type(val code: Int, val jobQueue: String, val inAppPaymentType: InAppPaymentType) {
enum class Type(val code: Int, val jobQueue: String, val inAppPaymentType: InAppPaymentType, val lock: Lock = ReentrantLock()) {
/**
* A recurring donation
*/

View File

@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import kotlin.concurrent.withLock
/**
* Checks and rectifies state pertaining to backups subscriptions.
@@ -85,7 +86,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
synchronized(InAppPaymentSubscriberRecord.Type.BACKUP) {
InAppPaymentSubscriberRecord.Type.BACKUP.lock.withLock {
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
if (inAppPayment?.state == InAppPaymentTable.State.PENDING) {

View File

@@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
/**
* Job to redeem a verified donation receipt. It is up to the Job prior in the chain to specify a valid
@@ -157,8 +158,12 @@ public class DonationReceiptRedemptionJob extends BaseJob {
@Override
protected void onRun() throws Exception {
if (isForSubscription()) {
synchronized (InAppPaymentSubscriberRecord.Type.DONATION) {
Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock();
lock.lock();
try {
doRun();
} finally {
lock.unlock();
}
} else {
doRun();

View File

@@ -167,7 +167,7 @@ class ExternalLaunchDonationJob private constructor(
subscriptionLevel,
subscriber.currency.currencyCode,
levelUpdateOperation.idempotencyKey.serialize(),
subscriber.type
subscriber.type.lock
)
getResultOrThrow(updateSubscriptionLevelResponse, doOnApplicationError = {

View File

@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.subscription.LevelUpdate
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
import org.thoughtcrime.securesms.util.Environment
import org.whispersystems.signalservice.internal.ServiceResponse
import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.days
/**
@@ -77,7 +78,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
var hasRetry = false
for (payment in unauthorizedInAppPayments) {
val verificationStatus: CheckResult<Unit> = if (payment.type.recurring) {
synchronized(payment.type.requireSubscriberType().inAppPaymentType) {
payment.type.requireSubscriberType().lock.withLock {
checkRecurringPayment(payment)
}
} else {
@@ -244,7 +245,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
level,
subscriber.currency.currencyCode,
updateOperation.idempotencyKey.serialize(),
subscriber.type
subscriber.type.lock
)
val updateLevelResult = checkResult(updateLevelResponse)

View File

@@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.internal.EmptyResponse
import org.whispersystems.signalservice.internal.ServiceResponse
import java.util.Locale
import kotlin.concurrent.withLock
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
@@ -90,7 +91,7 @@ class InAppPaymentKeepAliveJob private constructor(
}
override fun onRun() {
synchronized(type) {
type.lock.withLock {
doRun()
}
}

View File

@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import kotlin.concurrent.withLock
/**
* Submits a purchase token to the server to link it with a subscriber id.
@@ -76,7 +77,7 @@ class InAppPaymentPurchaseTokenJob private constructor(
}
override fun onRun() {
synchronized(InAppPaymentsRepository.resolveMutex(inAppPaymentId)) {
InAppPaymentsRepository.resolveLock(inAppPaymentId).withLock {
doRun()
}
}
@@ -87,7 +88,7 @@ class InAppPaymentPurchaseTokenJob private constructor(
val response = AppDependencies.donationsService.linkGooglePlayBillingPurchaseTokenToSubscriberId(
inAppPayment.subscriberId!!,
inAppPayment.data.redemption!!.googlePlayBillingPurchaseToken!!,
InAppPaymentSubscriberRecord.Type.BACKUP
InAppPaymentSubscriberRecord.Type.BACKUP.lock
)
if (response.applicationError.isPresent) {

View File

@@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.Sub
import org.whispersystems.signalservice.internal.ServiceResponse
import java.io.IOException
import java.util.Currency
import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@@ -128,7 +129,7 @@ class InAppPaymentRecurringContextJob private constructor(
}
override fun onRun() {
synchronized(InAppPaymentsRepository.resolveMutex(inAppPaymentId)) {
InAppPaymentsRepository.resolveLock(inAppPaymentId).withLock {
doRun()
}
}

View File

@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.util.hasGiftBadge
import org.thoughtcrime.securesms.util.requireGiftBadge
import org.whispersystems.signalservice.internal.ServiceResponse
import java.io.IOException
import kotlin.concurrent.withLock
/**
* Takes a ReceiptCredentialResponse and submits it to the server for redemption.
@@ -181,7 +182,7 @@ class InAppPaymentRedemptionJob private constructor(
}
if (inAppPayment.type.recurring) {
synchronized(inAppPayment.type.requireSubscriberType()) {
inAppPayment.type.requireSubscriberType().lock.withLock {
performInAppPaymentRedemption(inAppPayment)
}
} else {

View File

@@ -20,6 +20,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import okio.ByteString;
@@ -57,8 +58,12 @@ public class SubscriptionKeepAliveJob extends BaseJob {
@Override
protected void onRun() throws Exception {
synchronized (InAppPaymentSubscriberRecord.Type.DONATION) {
Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock();
lock.lock();
try {
doRun();
} finally {
lock.unlock();
}
}

View File

@@ -39,6 +39,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import okio.ByteString;
@@ -140,8 +141,12 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
@Override
protected void onRun() throws Exception {
synchronized (InAppPaymentSubscriberRecord.Type.DONATION) {
Lock lock = InAppPaymentSubscriberRecord.Type.DONATION.getLock();
lock.lock();
try {
doRun();
} finally {
lock.unlock();
}
}

View File

@@ -33,6 +33,7 @@ import java.util.Currency
import java.util.Locale
import java.util.Optional
import java.util.concurrent.TimeUnit
import kotlin.concurrent.withLock
/**
* Key-Value store for in app payment related values. Note that most of this file will be deprecated after the release of
@@ -449,7 +450,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
*/
@WorkerThread
fun updateLocalStateForManualCancellation(subscriberType: InAppPaymentSubscriberRecord.Type) {
synchronized(subscriberType) {
subscriberType.lock.withLock {
Log.d(TAG, "[updateLocalStateForManualCancellation] Clearing donation values.")
clearLevelOperations()
@@ -493,7 +494,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
*/
@WorkerThread
fun updateLocalStateForLocalSubscribe(subscriberType: InAppPaymentSubscriberRecord.Type) {
synchronized(subscriberType) {
subscriberType.lock.withLock {
clearLevelOperations()
if (subscriberType == InAppPaymentSubscriberRecord.Type.DONATION) {