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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user