From 175dd6aa8b509d9e1a3141f7c553987145cd372e Mon Sep 17 00:00:00 2001 From: matt-ramotar Date: Thu, 17 Oct 2024 21:13:27 -0400 Subject: [PATCH] Add back test Signed-off-by: matt-ramotar --- paging/kover/coverage.xml | 28 +++--- .../store/store5/impl/RealMutableStore.kt | 11 +-- .../store5/StoreWithInMemoryCacheTests.kt | 94 +++++++++++++++++++ 3 files changed, 108 insertions(+), 25 deletions(-) diff --git a/paging/kover/coverage.xml b/paging/kover/coverage.xml index 13254ba4c..a23845937 100644 --- a/paging/kover/coverage.xml +++ b/paging/kover/coverage.xml @@ -2,20 +2,15 @@ - - - - - - + - - + + @@ -278,10 +273,9 @@ - - + - + @@ -449,10 +443,10 @@ - + - - + + @@ -1066,10 +1060,10 @@ - + - - + + diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt index 84fd02948..d798d57fe 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt @@ -201,16 +201,11 @@ internal class RealMutableStore withThreadSafety( key: Key, block: suspend ThreadSafety.() -> Output, - ): Output { - storeLock.lock() - try { + ): Output = + storeLock.withLock { val threadSafety = requireNotNull(keyToThreadSafety[key]) - val output = threadSafety.block() - return output - } finally { - storeLock.unlock() + threadSafety.block() } - } private suspend fun conflictsMightExist(key: Key): Boolean { val lastFailedSync = bookkeeper?.getLastFailedSync(key) diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt index b50ec9703..119ccd3a0 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt @@ -2,13 +2,20 @@ package org.mobilenativefoundation.store.store5 import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.impl.extensions.get import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.hours +@OptIn(ExperimentalStoreApi::class) @FlowPreview @ExperimentalCoroutinesApi class StoreWithInMemoryCacheTests { @@ -39,4 +46,91 @@ class StoreWithInMemoryCacheTests { assertEquals("result", c) assertEquals("result", d) } + + @Test + fun storeDeadlock() = + runTest { + repeat(100) { + val store: MutableStore = + StoreBuilder + .from( + fetcher = Fetcher.of { key: Int -> "fetcher_$key" }, + sourceOfTruth = + SourceOfTruth.of( + reader = { key: Int -> + flowOf("source_of_truth_$key") + }, + writer = { key: Int, local: String -> }, + ), + ) + .disableCache() + .toMutableStoreBuilder( + converter = + object : Converter { + override fun fromNetworkToLocal(network: String): String = network + + override fun fromOutputToLocal(output: String): String = output + }, + ) + .build( + updater = + object : Updater { + var callCount = -1 + + override suspend fun post( + key: Int, + value: String, + ): UpdaterResult { + callCount += 1 + return if (callCount % 2 == 0) { + throw IllegalArgumentException("$key value: $value") + } else { + UpdaterResult.Success.Untyped("") + } + } + + override val onCompletion: OnUpdaterCompletion? = null + }, + ) + + val jobs = mutableListOf() + jobs.add( + store.stream(StoreReadRequest.cached(1, refresh = true)) + .mapNotNull { it.dataOrNull() } + .launchIn(this), + ) + val job1 = + store.stream(StoreReadRequest.cached(0, refresh = true)) + .mapNotNull { it.dataOrNull() } + .launchIn(this) + jobs.add( + store.stream(StoreReadRequest.cached(2, refresh = true)) + .mapNotNull { it.dataOrNull() } + .launchIn(this), + ) + jobs.add( + store.stream(StoreReadRequest.cached(3, refresh = true)) + .mapNotNull { it.dataOrNull() } + .launchIn(this), + ) + job1.cancel() + assertEquals( + expected = "source_of_truth_0", + actual = + store.stream(StoreReadRequest.cached(0, refresh = true)) + .mapNotNull { it.dataOrNull() } + .first(), + ) + jobs.forEach { + it.cancel() + assertEquals( + expected = "source_of_truth_0", + actual = + store.stream(StoreReadRequest.cached(0, refresh = true)) + .mapNotNull { it.dataOrNull() } + .first(), + ) + } + } + } }