Skip to content

Commit

Permalink
AtomicList and AtomicSet implementations (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
cedrickcooke authored Feb 2, 2024
1 parent ba5b9d6 commit 4767dbd
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 12 deletions.
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,14 @@ Toolbox of utilities/helpers for Kotlin development.
![badge-jvm]
![badge-mac]

### [`SynchronizedMap`]
### `AtomicList`, `AtomicSet`, and `AtomicMap`

Provides a map where read and write operations are protected using a reentrant lock, allowing mutation from multiple
threads without fear of data corruption.
Implementations of `List`, `Set`, and `Map` with strong guarantees around mutability. Each of these collections can be `snapshot` to reference their current values without reflecting future changes. A `StateFlow` of `snapshots` is accessible for receiving hot notifications of mutations. Returned collections and iterators automatically reference the `snapshot` of when they were created.

Use `SynchronizedMap.synchronize` when multiple read and write operations need to happen atomically, such as when
performing a `getOrPut`.

```kotlin
// Creating a synchronized map
val map = SynchronizedMap(mutableMapOf("key" to "value"))
// Synchronize across multiple operations
val value = map.synchronized { getOrPut("key") { "defaultValue" } }
```
These collections do not implement the various mutable collection interfaces. To mutate these collections, you must use an explicit mutator function. These mutator functions use a lambda to modify the list, and if concurrent mutations occur these lambdas may be ran more than once. In this way each mutation is guaranteed _atomic_, but you must be careful with side effects.
- `mutate` updates the collection without returning a value.
- `snapshotAndMutate` updates the collection and returns the snapshot which was used in the successful mutation.
- `mutateAndSnapshot` updates the collection and returns the snapshot which results from the mutation.

### [`Map.toJsObject`]

Expand Down
81 changes: 81 additions & 0 deletions collections/api/collections.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
public final class com/juul/tuulbox/collections/AtomicList : java/util/List, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lkotlinx/collections/immutable/PersistentList;)V
public fun add (ILjava/lang/Object;)V
public fun add (Ljava/lang/Object;)Z
public fun addAll (ILjava/util/Collection;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun equals (Ljava/lang/Object;)Z
public fun get (I)Ljava/lang/Object;
public fun getSize ()I
public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableList;
public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow;
public fun hashCode ()I
public fun indexOf (Ljava/lang/Object;)I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun lastIndexOf (Ljava/lang/Object;)I
public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)Ljava/util/ListIterator;
public final fun mutate (Lkotlin/jvm/functions/Function1;)V
public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList;
public fun remove (I)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun replaceAll (Ljava/util/function/UnaryOperator;)V
public fun retainAll (Ljava/util/Collection;)Z
public fun set (ILjava/lang/Object;)Ljava/lang/Object;
public final fun size ()I
public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableList;
public fun sort (Ljava/util/Comparator;)V
public synthetic fun subList (II)Ljava/util/List;
public fun subList (II)Lkotlinx/collections/immutable/ImmutableList;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}

public final class com/juul/tuulbox/collections/AtomicListKt {
public static final fun atomicListOf ()Lcom/juul/tuulbox/collections/AtomicList;
public static final fun atomicListOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicList;
}

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
Expand Down Expand Up @@ -40,11 +85,47 @@ public final class com/juul/tuulbox/collections/AtomicMap : java/util/Map, kotli
}

public final class com/juul/tuulbox/collections/AtomicMapKt {
public static final fun atomicHashMapOf ()Lcom/juul/tuulbox/collections/AtomicMap;
public static final fun atomicHashMapOf ([Lkotlin/Pair;)Lcom/juul/tuulbox/collections/AtomicMap;
public static final fun atomicMapOf ()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/AtomicSet : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lkotlinx/collections/immutable/PersistentSet;)V
public fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun equals (Ljava/lang/Object;)Z
public fun getSize ()I
public final fun getSnapshot ()Lkotlinx/collections/immutable/ImmutableSet;
public final fun getSnapshots ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getState ()Lkotlinx/coroutines/flow/MutableStateFlow;
public fun hashCode ()I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun mutate (Lkotlin/jvm/functions/Function1;)V
public final fun mutateAndSnapshot (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableSet;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public final fun snapshotAndMutate (Lkotlin/jvm/functions/Function1;)Lkotlinx/collections/immutable/ImmutableSet;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}

public final class com/juul/tuulbox/collections/AtomicSetKt {
public static final fun atomicHashSetOf ()Lcom/juul/tuulbox/collections/AtomicSet;
public static final fun atomicHashSetOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicSet;
public static final fun atomicSetOf ()Lcom/juul/tuulbox/collections/AtomicSet;
public static final fun atomicSetOf ([Ljava/lang/Object;)Lcom/juul/tuulbox/collections/AtomicSet;
}

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
89 changes: 89 additions & 0 deletions collections/src/commonMain/kotlin/AtomicList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.juul.tuulbox.collections

import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
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 empty [AtomicList]. */
public fun <E> atomicListOf(): AtomicList<E> = AtomicList(persistentListOf())

/** Returns an [AtomicList]. */
public fun <E> atomicListOf(
vararg elements: E,
): AtomicList<E> = AtomicList(persistentListOf(*elements))

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

/** Construct an [AtomicList] with [initial] list. */
public constructor(initial: PersistentList<E>) : this(MutableStateFlow(initial))

/** Returns this list as a [StateFlow]. Each mutation will cause a new emission on this flow. */
public val snapshots: StateFlow<ImmutableList<E>> = state.asStateFlow()

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

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

override fun get(index: Int): E = snapshot[index]

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

override fun iterator(): Iterator<E> = snapshot.iterator()

override fun listIterator(): ListIterator<E> = snapshot.listIterator()

override fun listIterator(index: Int): ListIterator<E> = snapshot.listIterator(index)

override fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> = snapshot.subList(fromIndex, toIndex)

override fun lastIndexOf(element: E): Int = snapshot.lastIndexOf(element)

override fun indexOf(element: E): Int = snapshot.indexOf(element)

override fun containsAll(elements: Collection<E>): Boolean = snapshot.containsAll(elements)

override fun contains(element: E): Boolean = snapshot.contains(element)

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

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

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

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

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

/** Mutates this List atomically and returns the new [snapshot]. [mutator] can be evaluated multiple times if a concurrent edit occurs. */
public inline fun mutateAndSnapshot(mutator: MutableList<E>.() -> Unit): ImmutableList<E> =
state.updateAndGet { it.mutate(mutator) }
}
6 changes: 6 additions & 0 deletions collections/src/commonMain/kotlin/AtomicMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet

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

/** 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 empty [AtomicMap] that does not guarantee preservation of iteration order, but may be faster. */
public fun <K, V> atomicHashMapOf(): AtomicMap<K, V> = AtomicMap(persistentHashMapOf())

/** 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>,
Expand Down
86 changes: 86 additions & 0 deletions collections/src/commonMain/kotlin/AtomicSet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.juul.tuulbox.collections

import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentHashSetOf
import kotlinx.collections.immutable.persistentSetOf
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 empty [AtomicSet] that guarantees preservation of iteration order, but may be slower. */
public fun <E> atomicSetOf(): AtomicSet<E> = AtomicSet(persistentSetOf())

/** Returns an [AtomicSet] that guarantees preservation of iteration order, but may be slower. */
public fun <E> atomicSetOf(
vararg elements: E,
): AtomicSet<E> = AtomicSet(persistentSetOf(*elements))

/** Returns an empty [AtomicSet] that does not guarantee preservation of iteration order, but may be faster. */
public fun <E> atomicHashSetOf(): AtomicSet<E> = AtomicSet(persistentHashSetOf())

/** Returns an [AtomicSet] that does not guarantee preservation of iteration order, but may be faster. */
public fun <E> atomicHashSetOf(
vararg elements: E,
): AtomicSet<E> = AtomicSet(persistentHashSetOf(*elements))

/**
* A [Set] that allows for thread safe, atomic mutation. Returned [iterator] references a [snapshot]
* of when this was accessed, and is not mutated when the set is.
*
* Although mutable, this class intentionally does not implement [MutableSet]. Mutation must use
* designated mutator functions ([mutate], [snapshotAndMutate], [mutateAndSnapshot]).
*/
public class AtomicSet<E> private constructor(
@PublishedApi
internal val state: MutableStateFlow<PersistentSet<E>>,
) : Set<E> {

/** Construct an [AtomicSet] with [initial] set. */
public constructor(initial: PersistentSet<E>) : this(MutableStateFlow(initial))

/** Returns this set as a [StateFlow]. Each mutation will cause a new emission on this flow. */
public val snapshots: StateFlow<ImmutableSet<E>> = state.asStateFlow()

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

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

override fun containsAll(elements: Collection<E>): Boolean = snapshot.containsAll(elements)

override fun contains(element: E): Boolean = snapshot.contains(element)

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

override fun iterator(): Iterator<E> = snapshot.iterator()

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

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

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

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

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

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

0 comments on commit 4767dbd

Please sign in to comment.