Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-12 16:18:53 +00:00
parent 567cc9541d
commit 1da2ca6d1f
4 changed files with 87 additions and 44 deletions

View file

@ -3,15 +3,21 @@ package com.tangem.data.common.cache
import com.tangem.datasource.local.cache.CacheKeysStore
import com.tangem.datasource.local.cache.model.CacheKey
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import org.joda.time.LocalDateTime
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
internal class DefaultCacheRegistry(
private val cacheKeysStore: CacheKeysStore,
) : CacheRegistry {
private val mutex = Mutex()
private val mutexes = ConcurrentHashMap<String, Mutex>()
override suspend fun isExpired(key: String): Boolean {
val cacheKey = cacheKeysStore.getSyncOrNull(key) ?: return true
@ -41,27 +47,36 @@ internal class DefaultCacheRegistry(
expireIn: Duration,
block: suspend () -> Unit,
) {
val isExpired = isExpired(key) || skipCache
if (!isExpired) return
// use a separate mutexForKey for each key to avoid multiple calls block() to the same key
// also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key
val mutexForKey = mutex.withLock {
mutexes.getOrPut(key) { Mutex() }
}
mutexForKey.withLock {
val isExpired = isExpired(key) || skipCache
if (!isExpired) {
return
}
try {
Timber.d("Invoke the action associated with the cache key: $key")
try {
Timber.d("Invoke the action associated with the cache key: $key")
cacheKeysStore.store(
key = CacheKey(
id = key,
updatedAt = LocalDateTime.now(),
expiresIn = expireIn,
),
)
cacheKeysStore.store(
key = CacheKey(
id = key,
updatedAt = LocalDateTime.now(),
expiresIn = expireIn,
),
)
block()
} catch (e: Throwable) {
Timber.e(e, "The action related to the cache key has failed: $key")
block()
} catch (e: Throwable) {
Timber.e(e, "The action related to the cache key has failed: $key")
invalidate(key)
invalidate(key)
throw e
throw e
}
}
}
}

View file

@ -56,8 +56,9 @@ internal class DefaultNetworksRepository(
.cancellable()
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
val currencies = getCurrencies(userWalletId, networks)
withContext(dispatchers.io) {
fetchNetworksPendingTransactions(userWalletId, networks)
fetchNetworksPendingTransactions(userWalletId, networks, currencies)
}
}
@ -80,23 +81,28 @@ internal class DefaultNetworksRepository(
networks: Set<Network>,
refresh: Boolean,
) {
val currencies = getCurrencies(userWalletId, networks)
coroutineScope {
networks
.map { network ->
async {
fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh)
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
}
}
.awaitAll()
}
}
private suspend fun fetchNetworksPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
private suspend fun fetchNetworksPendingTransactions(
userWalletId: UserWalletId,
networks: Set<Network>,
currencies: Sequence<CryptoCurrency>,
) {
coroutineScope {
networks
.map { network ->
async {
fetchNetworkPendingTransactions(userWalletId, network)
fetchNetworkPendingTransactions(userWalletId, network, currencies)
}
}
.awaitAll()
@ -106,22 +112,28 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworkStatusIfCacheExpired(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId, network),
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network) },
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
}
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
val currencies = getCurrencies(userWalletId, network)
private suspend fun fetchNetworkStatus(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
) {
val result = walletManagersFacade.update(
userWalletId = userWalletId,
network = network,
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
extraTokens = currencies
.filterIsInstance<CryptoCurrency.Token>()
.filter { it.network == network }
.toSet(),
)
withContext(NonCancellable) {
@ -137,9 +149,11 @@ internal class DefaultNetworksRepository(
networksStatusesStore.store(userWalletId, networkStatus)
}
private suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, network: Network) {
val currencies = getCurrencies(userWalletId, network)
private suspend fun fetchNetworkPendingTransactions(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
) {
val result = walletManagersFacade.updatePendingTransactions(
userWalletId = userWalletId,
network = network,
@ -158,7 +172,7 @@ internal class DefaultNetworksRepository(
networksStatusesStore.store(userWalletId, networkStatus)
}
private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence<CryptoCurrency> {
private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set<Network>): Sequence<CryptoCurrency> {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"
}
@ -180,7 +194,7 @@ internal class DefaultNetworksRepository(
}
}
return currencies.filter { it.network == network }
return currencies.filter { networks.contains(it.network) }
}
private suspend fun invalidateCacheKeyIfNeeded(

View file

@ -17,6 +17,8 @@ import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
internal class DefaultQuotesRepository(
@ -32,6 +34,7 @@ internal class DefaultQuotesRepository(
@Volatile
private var quotesFetchedForAppCurrency: String? = null
private val mutex = Mutex()
@OptIn(ExperimentalCoroutinesApi::class)
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
@ -42,7 +45,6 @@ internal class DefaultQuotesRepository(
.filterNotNull()
.flatMapLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
quotesStore.get(currenciesIds).map(quotesConverter::convertSet)
}
.cancellable()
@ -79,15 +81,19 @@ internal class DefaultQuotesRepository(
appCurrencyId: String,
refresh: Boolean,
) {
val expiredCurrenciesIds = filterExpiredCurrenciesIds(
currenciesIds = currenciesIds,
refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId,
)
if (expiredCurrenciesIds.isEmpty()) return
// TODO("[REDACTED_JIRA]") need refactor working with quotesFetchedForAppCurrency,
// it changes after filterExpiredCurrenciesIds
// calls with different coroutines and lead to fetchQuotes
mutex.withLock {
val expiredCurrenciesIds = filterExpiredCurrenciesIds(
currenciesIds = currenciesIds,
refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId,
)
if (expiredCurrenciesIds.isEmpty()) return
quotesFetchedForAppCurrency = appCurrencyId
fetchQuotes(expiredCurrenciesIds, appCurrencyId)
quotesFetchedForAppCurrency = appCurrencyId
fetchQuotes(expiredCurrenciesIds, appCurrencyId)
}
}
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
@ -118,7 +124,6 @@ internal class DefaultQuotesRepository(
): Set<String> {
return currenciesIds.fold(hashSetOf()) { acc, currencyId ->
val rawCurrencyId = currencyId.rawCurrencyId
if (rawCurrencyId != null && rawCurrencyId !in acc) {
cacheRegistry.invokeOnExpire(
key = getQuoteCacheKey(rawCurrencyId),