Fix ConcurrentModificationException in RecipePagingSourceFactory

It seems that it is possible to launch several coroutines on same
main thread of application. That's why it is possible to launch both
invoke and invalidate at the same time even though they are marked as
synchronized. To fix the issue this commit uses a concurrent collection
instead of synchronization.
This commit is contained in:
Kirill Kamakin
2021-11-20 21:13:26 +03:00
parent 965b488eb4
commit 70c0df1cf7
2 changed files with 49 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ import androidx.paging.PagingSource
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import timber.log.Timber
import java.util.concurrent.ConcurrentSkipListSet
import javax.inject.Inject
import javax.inject.Singleton
@@ -11,9 +12,9 @@ import javax.inject.Singleton
class RecipePagingSourceFactory @Inject constructor(
private val recipeStorage: RecipeStorage
) : () -> PagingSource<Int, RecipeSummaryEntity> {
private val sources: MutableList<PagingSource<Int, RecipeSummaryEntity>> = mutableListOf()
private val sources: MutableSet<PagingSource<Int, RecipeSummaryEntity>> =
ConcurrentSkipListSet(PagingSourceComparator)
@Synchronized
override fun invoke(): PagingSource<Int, RecipeSummaryEntity> {
Timber.v("invoke() called")
val newSource = recipeStorage.queryRecipes()
@@ -21,7 +22,6 @@ class RecipePagingSourceFactory @Inject constructor(
return newSource
}
@Synchronized
fun invalidate() {
Timber.v("invalidate() called")
for (source in sources) {
@@ -31,4 +31,15 @@ class RecipePagingSourceFactory @Inject constructor(
}
sources.removeAll { it.invalid }
}
private object PagingSourceComparator : Comparator<PagingSource<Int, RecipeSummaryEntity>> {
override fun compare(
left: PagingSource<Int, RecipeSummaryEntity>?,
right: PagingSource<Int, RecipeSummaryEntity>?
): Int {
val leftHash = left?.hashCode() ?: 0
val rightHash = right?.hashCode() ?: 0
return leftHash - rightHash
}
}
}

View File

@@ -0,0 +1,35 @@
package gq.kirmanak.mealient.data.recipes.impl
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.test.HiltRobolectricTest
import kotlinx.coroutines.*
import org.junit.Before
import org.junit.Test
import javax.inject.Inject
@ExperimentalCoroutinesApi
@HiltAndroidTest
class RecipePagingSourceFactoryTest : HiltRobolectricTest() {
@Inject
lateinit var storage: RecipeStorage
lateinit var subject: RecipePagingSourceFactory
@Before
fun setUp() {
subject = RecipePagingSourceFactory(storage)
}
@Test
fun `when modifying concurrently then doesn't throw`(): Unit = runBlocking {
(0..100).map {
async(Dispatchers.Default) {
for (i in 0..100) {
subject.invalidate()
subject.invoke()
}
}
}.awaitAll()
}
}