Kotlin / kotlinx.collections.immutable

Immutable persistent collections for Kotlin
Apache License 2.0
1.12k stars 56 forks source link

Ghost element when applying multiples minus operations on a PersistentHashSet #144

Open correacaio opened 1 year ago

correacaio commented 1 year ago

Hi everyone.

We have been using kotlinx collections immutable for some years and we finally found a bug :worried: I don't know exactly why it happens, but I can simulate it with the following code:

(1..1000).forEach {   // the bug doesn't happen every time, so I put it inside a loop to make it easier to happen 
    val originalList = (1..40000)
        .map {
            listOf(
                UUID.randomUUID().toString(),
                UUID.randomUUID().toString()
            )
        }

    val originalPersistentSet = originalList.flatten()
        .toPersistentList()
        .add(UUID.randomUUID().toString())  // this is the only element that shouldn't be removed
        .toPersistentHashSet()

    val firstLBatchToRemove = originalList.map { it[0] }.toPersistentHashSet()
    val secondBatchToRemove = originalList.map { it[1] }.toPersistentHashSet()

    val result = originalPersistentSet
    //  .minus(firstLBatchToRemove.plus(secondBatchToRemove).toPersistentHashSet())  // if I use this line instead of the next two, it seems that the bug doesn't happen anymore
        .minus(firstLBatchToRemove)
        .minus(secondBatchToRemove)
        .toPersistentHashSet()

    result.size shouldBe 1  // always true, as it should be
    shouldNotThrowAny { println(result.first()) }    // This assert fails once in a while throwing `NoSuchElementException`

    delay(1.seconds)
}

Basically, after two minus operations in a "large" PersistentHashSet, I get a PersistentSet with size 1 (as expected in this test). Still, when I try to get the element, the following exception is thrown:

Collection is empty.
java.util.NoSuchElementException: Collection is empty.
    at kotlin.collections.CollectionsKt___CollectionsKt.first(_Collections.kt:201)
    at com.collections.PersistentHashSetTest$1$2.invokeSuspend(PersistentHashSetTest.kt:59) // my test class
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)

It long as I can tell, it is a bug exclusively on PersistentHashSet. I've applied the same tests using PersistentSet, HashSet, and Lists (immutable or not) and it worked just fine.

Hope I have explained it successfully, please tell me if I can help more somehow.