First of all, thanks for a nice concurrent hashmap implementation, was glad to find one I could use in my multiplatform app!
Today I got a crash report from production which I narrowed down to a simple reproducing example attached and pasted below. I am not quite sure what is going on, but it reproduces every time so I guess this is some kind of edge case and not a race condition.
There are two examples which I think are related:
- using
suspendingWorker an exception is thrown (see below)
- using
blockingWorker the workers never finish even though one would expect them to do
Exception:
Exception in thread "DefaultDispatcher-worker-48" java.lang.ArrayIndexOutOfBoundsException: Index 64 out of bounds for length 64
at io.github.charlietap.leftright.LeftRight.waitForReaders(LeftRight.kt:60)
at io.github.charlietap.cachemap.InternalCacheMap.put(InternalCacheMap.kt:165)
at org.example.AppKt.suspendingWorker(App.kt:42)
at org.example.AppKt$suspendingWorker$1.invokeSuspend(App.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:113)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:89)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:589)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:823)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:720)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:707)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@1b38f3d3, Dispatchers.Default]
Example project to reproduce the issue. Use ./gradlew run to execute.
cachemap-bug-reproducer.zip
@OptIn(DelicateCoroutinesApi::class)
fun main(args: Array<String>) {
val cache: CacheMap<String, String> = cacheMapOf()
repeat(64) {
GlobalScope.launch {
withContext(Dispatchers.IO) {
suspendingWorker(cache)
// Note: Alternatively, using this worker seems to unexpectedly block at some point
// blockingWorker(cache)
}
}
}
Thread.sleep(100_000)
}
suspend fun suspendingWorker(cache: CacheMap<String, String>) {
while (true) {
val key = Random.nextInt(1000).toString()
val existing = cache.get(key)
if (existing == null) {
val value = GlobalScope.async {
UUID.randomUUID().toString()
}.await()
cache.put(key, value)
}
if (cache.size >= 998) {
break
}
}
}
fun blockingWorker(cache: CacheMap<String, String>) {
while (true) {
val key = Random.nextInt(1000).toString()
val existing = cache.get(key)
if (existing == null) {
cache.put(key, UUID.randomUUID().toString())
}
if (cache.size >= 998) {
// Note: code never reaches this break even though one would expect the cache to fill up at some point
break
}
println(cache.size)
}
}
First of all, thanks for a nice concurrent hashmap implementation, was glad to find one I could use in my multiplatform app!
Today I got a crash report from production which I narrowed down to a simple reproducing example attached and pasted below. I am not quite sure what is going on, but it reproduces every time so I guess this is some kind of edge case and not a race condition.
There are two examples which I think are related:
suspendingWorkeran exception is thrown (see below)blockingWorkerthe workers never finish even though one would expect them to doException:
Example project to reproduce the issue. Use
./gradlew runto execute.cachemap-bug-reproducer.zip