Skip to content

Commit

Permalink
AtomicMap implementation (#337)
Browse files Browse the repository at this point in the history
  • Loading branch information
cedrickcooke authored Feb 1, 2024
1 parent e10e8fd commit 33a571b
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 1 deletion.
47 changes: 47 additions & 0 deletions collections/api/collections.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
public final class com/juul/tuulbox/collections/AtomicMap : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lkotlinx/collections/immutable/PersistentMap;)V
public fun clear ()V
public fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;
public fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun containsKey (Ljava/lang/Object;)Z
public fun containsValue (Ljava/lang/Object;)Z
public synthetic fun entrySet ()Ljava/util/Set;
public final fun entrySet ()Lkotlinx/collections/immutable/ImmutableSet;
public fun equals (Ljava/lang/Object;)Z
public fun get (Ljava/lang/Object;)Ljava/lang/Object;
public fun getEntries ()Lkotlinx/collections/immutable/ImmutableSet;
public fun getKeys ()Lkotlinx/collections/immutable/ImmutableSet;
public fun getSize ()I
public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableMap;
public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow;
public fun getValues ()Lkotlinx/collections/immutable/ImmutableCollection;
public fun hashCode ()I
public fun isEmpty ()Z
public synthetic fun keySet ()Ljava/util/Set;
public final fun keySet ()Lkotlinx/collections/immutable/ImmutableSet;
public fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public final fun mutate (Lkotlin/jvm/functions/Function1;)V
public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableMap;
public fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun putAll (Ljava/util/Map;)V
public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replaceAll (Ljava/util/function/BiFunction;)V
public final fun size ()I
public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableMap;
public fun toString ()Ljava/lang/String;
public synthetic fun values ()Ljava/util/Collection;
public final fun values ()Lkotlinx/collections/immutable/ImmutableCollection;
}

public final class com/juul/tuulbox/collections/AtomicMapKt {
public static final fun atomicHashMapOf ([Lkotlin/Pair;)Lcom/juul/tuulbox/collections/AtomicMap;
public static final fun atomicMapOf ([Lkotlin/Pair;)Lcom/juul/tuulbox/collections/AtomicMap;
public static final fun getOrPut (Lcom/juul/tuulbox/collections/AtomicMap;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
}

public final class com/juul/tuulbox/collections/SynchronizedMap : java/util/Map, kotlin/jvm/internal/markers/KMutableMap {
public fun <init> ()V
public fun <init> (I)V
Expand Down
8 changes: 7 additions & 1 deletion collections/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ kotlin {
iosSimulatorArm64()

sourceSets {
val commonMain by getting { }
val commonMain by getting {
dependencies {
api(libs.kotlinx.collections.immutable)
api(libs.kotlinx.coroutines.core)
}
}

val commonTest by getting {
dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
Expand Down
95 changes: 95 additions & 0 deletions collections/src/commonMain/kotlin/AtomicMap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.juul.tuulbox.collections

import kotlinx.collections.immutable.ImmutableCollection
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentHashMapOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet

/** Returns an [AtomicMap] that guarantees preservation of iteration order, but may be slower. */
public fun <K, V> atomicMapOf(
vararg pairs: Pair<K, V>,
): AtomicMap<K, V> = AtomicMap(persistentMapOf(*pairs))

/** Returns an [AtomicMap] that does not guarantee preservation of iteration order, but may be faster. */
public fun <K, V> atomicHashMapOf(
vararg pairs: Pair<K, V>,
): AtomicMap<K, V> = AtomicMap(persistentHashMapOf(*pairs))

/**
* A [Map] that allows for thread safe, atomic mutation. Returned collections such as [entries] and
* [iterator] reference a [snapshot] of when they were accessed, and are not mutated when the map is.
*
* Although mutable, this class intentionally does not implement [MutableMap]. Mutation must use
* designated mutator functions ([mutate], [snapshotAndMutate], [mutateAndSnapshot]).
*/
public class AtomicMap<K, V> private constructor(
@PublishedApi
internal val state: MutableStateFlow<PersistentMap<K, V>>,
) : Map<K, V> {

/** Construct an [AtomicMap] with [initial] mappings. */
public constructor(initial: PersistentMap<K, V>) : this(MutableStateFlow(initial))

/** Returns this map as a [StateFlow]. Each mutation will cause a new emission on this flow. */
public val snapshots: StateFlow<ImmutableMap<K, V>> = state.asStateFlow()

/**
* Returns the current value of this map as an [immutable][ImmutableMap] snapshot.
*
* This operation is non-copying and efficient.
*/
public val snapshot: ImmutableMap<K, V>
get() = snapshots.value

override val size: Int
get() = snapshot.size

override val entries: ImmutableSet<Map.Entry<K, V>>
get() = snapshot.entries

override val keys: ImmutableSet<K>
get() = snapshot.keys

override val values: ImmutableCollection<V>
get() = snapshot.values

override fun containsKey(key: K): Boolean = snapshot.containsKey(key)

override fun containsValue(value: V): Boolean = snapshot.containsValue(value)

override fun get(key: K): V? = snapshot[key]

override fun isEmpty(): Boolean = snapshot.isEmpty()

override fun equals(other: Any?): Boolean = snapshot == other

override fun hashCode(): Int = snapshot.hashCode()

override fun toString(): String = snapshot.toString()

/** Mutates this map atomically. [mutator] can be evaluated multiple times if a concurrent edit occurs. */
public inline fun mutate(mutator: MutableMap<K, V>.() -> Unit) {
state.update { it.mutate(mutator) }
}

/** Mutates this map atomically and returns the previous [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */
public inline fun snapshotAndMutate(mutator: MutableMap<K, V>.() -> Unit): ImmutableMap<K, V> =
state.getAndUpdate { it.mutate(mutator) }

/** Mutates this map atomically and returns the new [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */
public inline fun mutateAndSnapshot(mutator: MutableMap<K, V>.() -> Unit): ImmutableMap<K, V> =
state.updateAndGet { it.mutate(mutator) }
}

/** Atomic version of [MutableMap.getOrPut]. [defaultValue] can be evaluated multiple times if a concurrent edit occurs. */
public inline fun <K, V> AtomicMap<K, V>.getOrPut(key: K, defaultValue: () -> V): V =
mutateAndSnapshot { getOrPut(key, defaultValue) }.getValue(key)
12 changes: 12 additions & 0 deletions collections/src/commonMain/kotlin/SynchronizedMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock

/** Returns an empty new [SynchronizedMap]. */
@Deprecated(
"Prefer using atomicMapOf instead. SynchronizedMap does not play nicely with coroutines.",
ReplaceWith("atomicMapOf", "com.juul.tuulbox.collections.atomicMapOf"),
)
public fun <K, V> synchronizedMapOf(): SynchronizedMap<K, V> =
SynchronizedMap(linkedMapOf())

/** Returns a new [SynchronizedMap] with the specified contents. */
@Deprecated(
"Prefer using atomicMapOf instead. SynchronizedMap does not play nicely with coroutines.",
ReplaceWith("atomicMapOf", "com.juul.tuulbox.collections.atomicMapOf"),
)
public fun <K, V> synchronizedMapOf(vararg pairs: Pair<K, V>): SynchronizedMap<K, V> =
SynchronizedMap(linkedMapOf(*pairs))

/** A [MutableMap] where all reads and writes are protected by a [ReentrantLock]. */
@Deprecated(
"Prefer using AtomicMap instead. SynchronizedMap does not play nicely with coroutines.",
ReplaceWith("AtomicMap", "com.juul.tuulbox.collections.AtomicMap"),
)
public class SynchronizedMap<K, V> internal constructor(
private val inner: LinkedHashMap<K, V>,
) : MutableMap<K, V> {
Expand Down
44 changes: 44 additions & 0 deletions collections/src/commonTest/kotlin/AtomicMapTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.juul.tuulbox.collections

import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

public class AtomicMapTests {

@Test
public fun atomicMap_concurrentMutateBlocks_doesNotLoseWrites() = runTest {
val actual = AtomicMap<String, Int>(persistentMapOf())
(1..10).map {
launch(Dispatchers.Default) {
for (i in 0 until 500) {
actual.mutate { this["count"] = (this["count"] ?: 0) + 1 }
}
}
}.joinAll()

assertEquals(mapOf("count" to 5_000), actual)
}

@Test
public fun atomicMap_snapshotAndMutate_returnsPreviousSnapshot() = runTest {
val atomic = AtomicMap<Int, Int>(persistentMapOf())
val actual = atomic.snapshotAndMutate {
put(0, 0)
}
assertEquals(emptyMap(), actual)
}

@Test
public fun atomicMap_mutateAndSnapshot_returnsNewSnapshot() = runTest {
val atomic = AtomicMap<Int, Int>(persistentMapOf())
val actual = atomic.mutateAndSnapshot {
put(0, 0)
}
assertEquals(mapOf(0 to 0), actual)
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ktor = "2.3.7"
[libraries]
androidx-core = { module = "androidx.core:core", version = "1.12.0" }
androidx-startup = { module = "androidx.startup:startup-runtime", version = "1.1.1" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
Expand Down

0 comments on commit 33a571b

Please sign in to comment.