Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit 6006738

Browse files
committed
feat: refactor UserList and UserListImpl for enhanced performance and thread safety
- Added `uuidSnapshot` method to `UserList` for generating point-in-time UUID snapshots. - Refactored `UserListImpl` to leverage `ConcurrentHashMap` for lock-free, weakly-consistent iteration. - Introduced caching mechanism with `modSeq` for efficient snapshot reuse in `UserListImpl`. - Updated `CloudPlayerManagerImpl` to use updated `UserListImpl.of` structure. - Improved efficiency and scalability of iteration, snapshotting, and mutation operations.
1 parent 0837873 commit 6006738

File tree

3 files changed

+247
-63
lines changed

3 files changed

+247
-63
lines changed
Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,78 @@
11
package dev.slne.surf.cloud.api.common.server
22

33
import dev.slne.surf.cloud.api.common.player.CloudPlayer
4-
import it.unimi.dsi.fastutil.objects.ObjectSet
4+
import it.unimi.dsi.fastutil.objects.ObjectArrayList
5+
import org.jetbrains.annotations.ApiStatus
56
import java.util.*
67

78
/**
8-
* Represents a list of users currently connected to a server.
9+
* Represents the *current* set of users connected to a server.
910
*
10-
* This list provides a view of the player data and allows snapshots to be created.
11-
* The list itself does not directly modify the state of the server.
11+
* ### Thread-safety and iteration semantics
12+
* Implementations of this interface are designed to be used concurrently.
13+
* The default implementation ([UserListImpl]) is backed by a
14+
* lock-free, weakly-consistent iterator (via a `ConcurrentHashMap`-based key set),
15+
* which means iteration does not block writes and will not throw [java.util.ConcurrentModificationException].
16+
* However, a running iteration may or may not reflect concurrent additions/removals.
17+
* If you require a stable, point-in-time view, call {@link #uuidSnapshot()} or {@link #snapshot()}.
18+
*
19+
* ### Mutability
20+
* This is a read-only view. To obtain a mutable copy that you can modify freely
21+
* (without affecting the underlying server state), use [snapshot] which returns
22+
* a [MutableUserList].
1223
*/
24+
@ApiStatus.NonExtendable
1325
interface UserList : Collection<CloudPlayer> {
1426

15-
val references: ObjectSet<UUID>
16-
1727
/**
18-
* Creates a mutable snapshot of the current user list.
28+
* Creates a mutable, point-in-time copy of the current user list.
1929
*
20-
* The snapshot is independent of the original list and can be modified
21-
* without affecting the server or the original user list.
30+
* The returned snapshot is independent from the live list; modifying the snapshot
31+
* has no effect on the server or the original list.
2232
*
23-
* @return A [MutableUserList] containing a copy of the current user list.
33+
* @return a new [MutableUserList] containing a copy of the current users.
2434
*/
2535
fun snapshot(): MutableUserList
36+
37+
/**
38+
* Returns a snapshot of all player UUIDs as a contiguous array-backed list.
39+
*
40+
* The returned list is safe to iterate without blocking writers and remains
41+
* stable even if the underlying live set changes after the call.
42+
*
43+
*
44+
* **Performance:** This method allocates a new list and copies all
45+
* UUIDs (O(n)). Prefer this when you need a consistent view across a longer
46+
* operation or when you plan to iterate multiple times.
47+
*
48+
* @return an [ObjectArrayList] containing the UUIDs at the time of the call.
49+
*/
50+
fun uuidSnapshot(): ObjectArrayList<UUID>
2651
}
2752

2853
/**
29-
* Represents a mutable list of users on a server.
54+
* A mutable user list.
55+
*
56+
* This type is intended for working with detached snapshots obtained via
57+
* [UserList.snapshot].
58+
* Mutating a [MutableUserList] does not affect
59+
* the server or the original live list.
60+
*/
61+
@ApiStatus.NonExtendable
62+
interface MutableUserList : UserList, MutableCollection<CloudPlayer>
63+
64+
/**
65+
* Performs a fast, lock-free traversal over the UUIDs of this list using the
66+
* default implementation's weakly-consistent iterator.
67+
*
68+
* **Important:** This extension relies on the instance being backed by
69+
* [UserListImpl]. If you pass a custom implementation, a [ClassCastException]
70+
* may occur. Use [UserList.uuidSnapshot] for a portable, implementation-agnostic
71+
* traversal.
3072
*
31-
* This interface allows modifications to the list, but any changes
32-
* will not affect the actual server or its state.
73+
* @receiver the user list to traverse (must be [UserListImpl]).
74+
* @param action callback invoked for each UUID visible to the iterator at traversal time.
3375
*/
34-
interface MutableUserList : UserList, MutableCollection<CloudPlayer>
76+
inline fun UserList.forEachWeakUuid(action: (UUID) -> Unit) {
77+
(this as UserListImpl).forEachWeakUuid(action)
78+
}

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/UserListImpl.kt

Lines changed: 188 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,108 +4,248 @@ import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodec
44
import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf
55
import dev.slne.surf.cloud.api.common.player.CloudPlayer
66
import dev.slne.surf.cloud.api.common.player.CloudPlayerManager
7+
import dev.slne.surf.cloud.api.common.server.UserListImpl.Companion.STREAM_CODEC
78
import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
8-
import dev.slne.surf.cloud.api.common.util.mutableObjectSetOf
9-
import dev.slne.surf.cloud.api.common.util.synchronize
10-
import dev.slne.surf.cloud.api.common.util.toObjectSet
11-
import it.unimi.dsi.fastutil.objects.ObjectSet
9+
import it.unimi.dsi.fastutil.objects.ObjectArrayList
10+
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
1211
import java.util.*
13-
12+
import java.util.concurrent.ConcurrentHashMap
13+
import java.util.concurrent.atomic.AtomicLong
14+
15+
/**
16+
* Default, high-throughput implementation of [UserList] optimized for
17+
* concurrent reads and frequent iteration in large Minecraft networks.
18+
*
19+
* ### Design rationale
20+
*
21+
* - **Concurrency**: Backed by [ConcurrentHashMap.newKeySet], providing
22+
* fine-grained striping/CAS and weakly-consistent iterators. Readers never block writers, which
23+
* is crucial when plugins may traverse thousands of players while joins/leaves occur.
24+
*
25+
* - **Stable wire format**: Serialization via [STREAM_CODEC] uses a
26+
* snapshot array to ensure a consistent view on the wire even during concurrent updates.
27+
*
28+
* - **Snapshot cache**: A lightweight modification counter ([modSeq]) invalidates a
29+
* cached array snapshot to avoid repeated allocations when the set is unchanged (e.g., multiple
30+
* serializations per tick).
31+
*
32+
* ### Iteration semantics
33+
* Iteration is weakly consistent: it never throws [java.util.ConcurrentModificationException],
34+
* but may omit or not yet include elements added/removed concurrently. Use [uuidSnapshot] for
35+
* a stable, point-in-time collection.
36+
*/
1437
@InternalApi
1538
open class UserListImpl : UserList {
1639
companion object {
40+
41+
/**
42+
* Binary codec for streaming a [UserListImpl] over [SurfByteBuf].
43+
*
44+
* **Consistency:** The codec writes a snapshot array captured at the start of encoding,
45+
* ensuring readers see a coherent list of UUIDs regardless of concurrent modifications.
46+
*/
1747
val STREAM_CODEC = streamCodec<SurfByteBuf, UserListImpl>({ buf, list ->
18-
buf.writeCollection(list.playerReferences) { buffer, uuid -> buffer.writeUuid(uuid) }
48+
val snapshot = list.snapshotArray()
49+
buf.writeArray(snapshot) { buffer, uuid -> buffer.writeUuid(uuid) }
1950
}, { buf ->
20-
UserListImpl(buf.readCollection({ mutableObjectSetOf(it) }, { it.readUuid() }))
51+
val uuids = buf.readArray { buf.readUuid() }
52+
UserListImpl(ObjectArrayList(uuids))
2153
})
2254

23-
fun of(players: Iterable<CloudPlayer>): UserListImpl {
24-
return UserListImpl(players.mapTo(mutableObjectSetOf()) { it.uuid })
55+
56+
/**
57+
* Creates a [UserListImpl] from a preexisting set of UUIDs.
58+
*
59+
* @param uuids UUIDs to initialize the list with.
60+
* @return a new [UserListImpl] containing the given UUIDs.
61+
*/
62+
fun of(uuids: Set<UUID>): UserListImpl {
63+
return UserListImpl(ObjectArrayList(uuids))
2564
}
2665
}
2766

28-
internal val playerReferences: ObjectSet<UUID>
67+
/** Backing set: scalable, lock-free key set. */
68+
@PublishedApi
69+
internal val uuids: ConcurrentHashMap.KeySetView<UUID, Boolean>
2970

30-
override val size get() = playerReferences.size
31-
override val references: ObjectSet<UUID>
32-
get() = playerReferences.toObjectSet()
71+
/** Modification counter used to invalidate the cached snapshot array. */
72+
protected val modSeq = AtomicLong(0)
3373

74+
@Volatile
75+
private var cachedSnapshot: Array<UUID> = emptyArray()
76+
77+
@Volatile
78+
private var cachedModSeq: Long = -1
79+
80+
/** Creates an empty user list. */
3481
constructor() {
35-
playerReferences = mutableObjectSetOf<UUID>().synchronize()
82+
uuids = ConcurrentHashMap.newKeySet<UUID>()
3683
}
3784

38-
internal constructor(playerReferences: ObjectSet<UUID>) {
39-
this.playerReferences = playerReferences.synchronize()
85+
/**
86+
* Creates a user list preloaded with UUIDs.
87+
*
88+
* @param initial UUIDs to preload into the set.
89+
*/
90+
internal constructor(initial: ObjectArrayList<UUID>) {
91+
uuids = ConcurrentHashMap.newKeySet<UUID>(initial.size)
92+
uuids.addAll(initial)
4093
}
4194

42-
override fun contains(element: CloudPlayer) = playerReferences.contains(element.uuid)
43-
override fun containsAll(elements: Collection<CloudPlayer>) =
44-
elements.all { playerReferences.contains(it.uuid) }
95+
override val size
96+
get() = uuids.size
4597

46-
override fun isEmpty() = playerReferences.isEmpty()
47-
override fun iterator(): Iterator<CloudPlayer> {
48-
return object : Iterator<CloudPlayer> {
49-
private val iterator = playerReferences.iterator()
50-
override fun hasNext() = iterator.hasNext()
51-
override fun next() = CloudPlayerManager.getPlayer(iterator.next())
52-
?: throw NoSuchElementException("Player not found")
53-
}
98+
override fun isEmpty() = uuids.isEmpty()
99+
override fun contains(element: CloudPlayer) = uuids.contains(element.uuid)
100+
override fun containsAll(elements: Collection<CloudPlayer>): Boolean {
101+
for (e in elements) if (!uuids.contains(e.uuid)) return false
102+
return true
54103
}
55104

56-
fun add(playerUuid: UUID): Boolean {
57-
return playerReferences.add(playerUuid)
105+
/**
106+
* Returns an iterator over players resolved on demand from [CloudPlayerManager].
107+
*
108+
* **Note:** If a UUID in the set cannot be resolved to a [CloudPlayer]
109+
* (e.g., the player disconnected between reading the UUID and resolution),
110+
* a [NoSuchElementException] is thrown from [next] to signal the race.
111+
*
112+
* @throws NoSuchElementException if a UUID cannot be resolved to a player at iteration time.
113+
*/
114+
override fun iterator() = object : Iterator<CloudPlayer> {
115+
private val iterator = uuids.iterator()
116+
override fun hasNext() = iterator.hasNext()
117+
override fun next() = CloudPlayerManager.getPlayer(iterator.next())
118+
?: throw NoSuchElementException("Player not found")
58119
}
59120

60-
fun remove(playerUuid: UUID): Boolean {
61-
return playerReferences.remove(playerUuid)
121+
/**
122+
* Implementation-specific fast traversal over UUIDs using the weakly-consistent iterator.
123+
*
124+
* @param action callback invoked for each UUID visible to the iterator.
125+
*/
126+
inline fun forEachWeakUuid(action: (UUID) -> Unit) {
127+
for (u in uuids) action(u)
62128
}
63129

64130
override fun snapshot(): MutableUserList {
65-
return MutableUserListImpl().also { it.addAll(this) }
131+
return MutableUserListImpl(uuidSnapshot())
132+
}
133+
134+
override fun uuidSnapshot() = ObjectArrayList(snapshotArray())
135+
136+
/**
137+
* Returns a cached snapshot array of UUIDs, invalidated whenever the set changes.
138+
*
139+
* **Performance:** Avoids repeated allocations between unchanged serializations.
140+
* The snapshot is coherent at the time of creation and unaffected by later mutations.
141+
*
142+
* @return array of UUIDs representing a point-in-time view.
143+
*/
144+
fun snapshotArray(): Array<UUID> {
145+
val m = modSeq.get()
146+
if (m == cachedModSeq) return cachedSnapshot
147+
val snap = uuids.toTypedArray()
148+
cachedSnapshot = snap
149+
cachedModSeq = m
150+
return snap
151+
}
152+
153+
154+
/**
155+
* Adds a player UUID to the live set.
156+
*
157+
* @param playerUuid the UUID to add.
158+
* @return `true` if the UUID was not present and has been added; `false` otherwise.
159+
*/
160+
fun add(playerUuid: UUID): Boolean {
161+
val added = uuids.add(playerUuid)
162+
if (added) {
163+
modSeq.incrementAndGet()
164+
}
165+
return added
166+
}
167+
168+
/**
169+
* Removes a player UUID from the live set.
170+
*
171+
* @param playerUuid the UUID to remove.
172+
* @return `true` if the UUID was present and has been removed; `false` otherwise.
173+
*/
174+
fun remove(playerUuid: UUID): Boolean {
175+
val removed = uuids.remove(playerUuid)
176+
if (removed) {
177+
modSeq.incrementAndGet()
178+
}
179+
return removed
66180
}
67181

68182
override fun toString(): String {
69-
return "UserListImpl(playerReferences=$playerReferences, size=$size)"
183+
return "UserListImpl(size=$size)"
70184
}
71185
}
72186

187+
188+
/**
189+
* Mutable snapshot implementation that can be freely modified without impacting the live set.
190+
*
191+
* ### Notes
192+
*
193+
* - [clear] increments [modSeq] to invalidate the internal snapshot cache.
194+
* - Mutating via the iterator's [MutableIterator.remove] also bumps [modSeq]
195+
* to ensure consistent snapshot invalidation.
196+
* - [retainAll] builds a temporary [ObjectOpenHashSet] of UUIDs to perform the set
197+
* operation efficiently.
198+
*/
73199
@InternalApi
74-
class MutableUserListImpl : UserListImpl(), MutableUserList {
200+
class MutableUserListImpl(initial: ObjectArrayList<UUID>) : UserListImpl(initial), MutableUserList {
75201
override fun add(element: CloudPlayer): Boolean {
76202
return add(element.uuid)
77203
}
78204

79205
override fun addAll(elements: Collection<CloudPlayer>): Boolean {
80-
return elements.all { playerReferences.add(it.uuid) }
206+
var changed = false
207+
for (e in elements) if (add(e.uuid)) changed = true
208+
return changed
81209
}
82210

83211
override fun clear() {
84-
playerReferences.clear()
212+
if (uuids.isNotEmpty()) {
213+
uuids.clear()
214+
modSeq.incrementAndGet()
215+
}
85216
}
86217

87-
override fun iterator(): MutableIterator<CloudPlayer> {
88-
return object : MutableIterator<CloudPlayer> {
89-
private val iterator = playerReferences.iterator()
90-
override fun hasNext() = iterator.hasNext()
91-
override fun next() = CloudPlayerManager.getPlayer(iterator.next())
92-
?: throw NoSuchElementException("Player not found")
93-
94-
override fun remove() {
95-
iterator.remove()
96-
}
218+
/**
219+
* Iterator over players resolved on demand. Removal through the iterator
220+
* invalidates the snapshot cache to keep [snapshotArray] fresh.
221+
*/
222+
override fun iterator() = object : MutableIterator<CloudPlayer> {
223+
private val iterator = uuids.iterator()
224+
override fun hasNext() = iterator.hasNext()
225+
override fun next() = CloudPlayerManager.getPlayer(iterator.next())
226+
?: throw NoSuchElementException("Player not found")
227+
228+
override fun remove() {
229+
iterator.remove()
230+
modSeq.incrementAndGet()
97231
}
98232
}
99233

234+
100235
override fun remove(element: CloudPlayer): Boolean {
101236
return remove(element.uuid)
102237
}
103238

104239
override fun removeAll(elements: Collection<CloudPlayer>): Boolean {
105-
return elements.all { playerReferences.remove(it.uuid) }
240+
var changed = false
241+
for (e in elements) if (remove(e.uuid)) changed = true
242+
return changed
106243
}
107244

108245
override fun retainAll(elements: Collection<CloudPlayer>): Boolean {
109-
return playerReferences.retainAll(elements.map { it.uuid })
246+
val keep = ObjectOpenHashSet<UUID>(elements.size)
247+
for (e in elements) keep.add(e.uuid)
248+
val changed = uuids.retainAll(keep)
249+
return changed
110250
}
111251
}

0 commit comments

Comments
 (0)