fix(quartz): snapshot LargeCache entries in forEach to prevent ConcurrentModificationException
On Apple (iOS/macOS) and Linux targets, LargeCache.forEach() iterates the underlying map directly. When another coroutine modifies the map during iteration (e.g., NostrClient.syncFilters running while subscriptions are added), a ConcurrentModificationException is thrown. On JVM/Android this is not an issue because ConcurrentSkipListMap handles concurrent iteration safely. On Kotlin/Native (iOS), this exception is fatal — K/N calls abort() for unhandled exceptions, crashing the app immediately after account creation when relays connect and subscriptions start syncing. Fix: call .entries.toList() before iterating to create a snapshot, matching the JVM behavior where concurrent modifications during iteration are tolerated.
This commit is contained in:
+4
-1
@@ -85,7 +85,10 @@ actual class LargeCache<K, V> : ICacheOperations<K, V> {
|
||||
actual override fun size(): Int = concurrentMap.size
|
||||
|
||||
actual override fun forEach(consumer: ICacheBiConsumer<K, V>) {
|
||||
concurrentMap.forEach { consumer.accept(it.key, it.value) }
|
||||
// Take a snapshot of entries to avoid ConcurrentModificationException
|
||||
// when the map is modified during iteration (e.g., NostrClient.syncFilters
|
||||
// iterating while subscriptions are added from another coroutine).
|
||||
concurrentMap.entries.toList().forEach { consumer.accept(it.key, it.value) }
|
||||
}
|
||||
|
||||
actual override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> =
|
||||
|
||||
+2
-1
@@ -88,7 +88,8 @@ actual class LargeCache<K, V> : ICacheOperations<K, V> {
|
||||
actual override fun size(): Int = withMap { it.size }
|
||||
|
||||
actual override fun forEach(consumer: ICacheBiConsumer<K, V>) {
|
||||
withMap { map -> map.forEach { consumer.accept(it.key, it.value) } }
|
||||
// Snapshot entries to avoid ConcurrentModificationException
|
||||
withMap { map -> map.entries.toList() }.forEach { consumer.accept(it.key, it.value) }
|
||||
}
|
||||
|
||||
actual override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> = withMap { map -> map.filter { consumer.filter(it.key, it.value) }.values.toList() }
|
||||
|
||||
Reference in New Issue
Block a user