From 250826951b0b5cd41af6af8aa66c8bf85409cf20 Mon Sep 17 00:00:00 2001 From: Fran Montiel Date: Tue, 3 Oct 2023 00:05:48 +0200 Subject: [PATCH] KMM :: Internal :: Make InMemoryDataSource thread-safe --- .../datasource/memory/InMemoryDataSource.kt | 49 +++++++++------ .../memory/InMemoryDataSourceTests.kt | 61 ++++++++++++++++--- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/harmony-kotlin/src/commonMain/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSource.kt b/harmony-kotlin/src/commonMain/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSource.kt index 3ccab69b..f7d0d415 100644 --- a/harmony-kotlin/src/commonMain/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSource.kt +++ b/harmony-kotlin/src/commonMain/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSource.kt @@ -8,40 +8,53 @@ import com.harmony.kotlin.data.query.KeyQuery import com.harmony.kotlin.data.query.Query import com.harmony.kotlin.error.DataNotFoundException import com.harmony.kotlin.error.notSupportedQuery +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock class InMemoryDataSource : GetDataSource, PutDataSource, DeleteDataSource { private val objects: MutableMap = mutableMapOf() + private val mutex = Mutex() override suspend fun get(query: Query): V = - when (query) { - is KeyQuery -> { - objects[query.key].run { - this ?: throw DataNotFoundException() + mutex.withLock { + when (query) { + is KeyQuery -> { + objects[query.key].run { + this ?: throw DataNotFoundException() + } } + + else -> notSupportedQuery() } - else -> notSupportedQuery() } override suspend fun put(query: Query, value: V?): V = - when (query) { - is KeyQuery -> { - value?.let { - objects.put(query.key, value).run { value } - } ?: throw IllegalArgumentException("InMemoryDataSource: value must be not null") + mutex.withLock { + when (query) { + is KeyQuery -> { + value?.let { + objects.put(query.key, value).run { value } + } ?: throw IllegalArgumentException("InMemoryDataSource: value must be not null") + } + + else -> notSupportedQuery() } - else -> notSupportedQuery() } override suspend fun delete(query: Query) { - when (query) { - is AllObjectsQuery -> { - objects.clear() - } - is KeyQuery -> { - clearAll(query.key) + mutex.withLock { + when (query) { + is AllObjectsQuery -> { + objects.clear() + } + + is KeyQuery -> { + clearAll(query.key) + } + + else -> notSupportedQuery() } - else -> notSupportedQuery() } } diff --git a/harmony-kotlin/src/commonTest/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSourceTests.kt b/harmony-kotlin/src/commonTest/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSourceTests.kt index 64a5d177..dda893ec 100644 --- a/harmony-kotlin/src/commonTest/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSourceTests.kt +++ b/harmony-kotlin/src/commonTest/kotlin/com/harmony/kotlin/data/datasource/memory/InMemoryDataSourceTests.kt @@ -2,22 +2,29 @@ package com.harmony.kotlin.data.datasource.memory +import arrow.atomic.AtomicInt import com.harmony.kotlin.common.BaseTest import com.harmony.kotlin.common.randomString import com.harmony.kotlin.data.query.KeyQuery import com.harmony.kotlin.data.query.VoidQuery import com.harmony.kotlin.error.DataNotFoundException import com.harmony.kotlin.error.QueryNotSupportedException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.fail class InMemoryDataSourceTests : BaseTest() { @Test fun `should throw DataNotFoundException if value is missing`() = runTest { assertFailsWith { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val query = KeyQuery(randomString()) inMemoryDataSource.get(query) @@ -27,7 +34,7 @@ class InMemoryDataSourceTests : BaseTest() { @Test fun `should throw QueryNotSupportedException if query is invalid when get function is called`() = runTest { assertFailsWith { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val invalidQuery = VoidQuery inMemoryDataSource.get(invalidQuery) @@ -57,7 +64,7 @@ class InMemoryDataSourceTests : BaseTest() { @Test fun `should throw QueryNotSupportedException if query is invalid when put function is called`() = runTest { assertFailsWith { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val invalidQuery = VoidQuery inMemoryDataSource.put(invalidQuery, randomString()) @@ -67,7 +74,7 @@ class InMemoryDataSourceTests : BaseTest() { @Test fun `should throw IllegalArgumentException if the value is null when put function is called`() = runTest { assertFailsWith { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val query = KeyQuery(randomString()) inMemoryDataSource.put(query, null) @@ -76,7 +83,7 @@ class InMemoryDataSourceTests : BaseTest() { @Test fun `should store value if query is valid when put function is called`() = runTest { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val query = KeyQuery(randomString()) val expectedValue = randomString() @@ -89,7 +96,7 @@ class InMemoryDataSourceTests : BaseTest() { @Test fun `should throw QueryNotSupportedException if query is invalid when delete function is called`() = runTest { assertFailsWith { - val inMemoryDataSource = givenInMemoryDataSource() + val inMemoryDataSource = givenInMemoryDataSource() val invalidQuery = VoidQuery inMemoryDataSource.delete(query = invalidQuery) @@ -120,10 +127,44 @@ class InMemoryDataSourceTests : BaseTest() { scope.inMemoryDataSource.put(pair.first, pair.second) } - private suspend fun givenInMemoryDataSource( - insertValue: Pair? = null, - ): InMemoryDataSource { - val inMemoryDataSource = InMemoryDataSource() + @Test + @Suppress("SwallowedException") + fun `should not have concurrency problems`() = kotlinx.coroutines.test.runTest { + val key = AtomicInt(0) + val inMemoryDataSource = givenInMemoryDataSource(Pair(KeyQuery(key.get().toString()), 0)) + + val numThreads = 10 + val numIterationsPerThread = 1000 + + val jobs = List(numThreads) { + launch(Dispatchers.IO) { + for (i in 0 until numIterationsPerThread) { + try { + inMemoryDataSource.put(KeyQuery(key.incrementAndGet().toString()), 0) + } catch (e: Exception) { + fail("Concurrency problem: putting element ${key.get()} failed with ${e.stackTraceToString()}") + } + } + } + } + + jobs.joinAll() + + // Ensure that the final state of the hashmap is consistent with expectations + val numberOfElements = numThreads * numIterationsPerThread + for (currentKey in 0..numberOfElements) { + try { + inMemoryDataSource.get(KeyQuery(currentKey.toString())) + } catch (e: DataNotFoundException) { + fail("Concurrency problem: element $currentKey not found because it was not inserted in the first place") + } + } + } + + private suspend fun givenInMemoryDataSource( + insertValue: Pair? = null, + ): InMemoryDataSource { + val inMemoryDataSource = InMemoryDataSource() insertValue?.let { inMemoryDataSource.put(it.first, it.second)