getExternalUUIDAsString() streams over PLAYERS.keySet() while other threads insert. That's a textbook ConcurrentModificationException, and a HashMap resize during a concurrent write can also corrupt the map. Iterating the cosmetic lists in the UI while a purchase completes has the same problem.
session-java runs notification handlers on a CompletableFuture.runAsync pool thread, so notifications always arrive off the main thread.
Fix
Either:
apply every state change on the client thread with Providers.saturn.getClient().executeOnThread(...), as the player worker already does in one place, or
switch to ConcurrentHashMap and CopyOnWriteArrayList. ConcurrentHashMap doesn't allow null values, so this goes together with the "not a Saturn user" marker from the get_player issue.
Moving writes onto the client thread is the more robust option, since the render code reads these without locks.
## Problem
Several collections are written from background threads and read on the render thread, with no synchronisation:
| Collection | Written from | Read from |
| --- | --- | --- |
| `SaturnPlayer.PLAYERS` (`HashMap`) | render thread (`get` → `put(uuid, null)`), auth thread (`SaturnPlayer.set`), the notification thread (`player` notification → `SaturnPlayer.set`) | render thread, `getExternalUUIDAsString()` |
| `Cloaks.availableCloaks`, `Hats.availableHats` (`ArrayList`) | auth thread (`authenticate`), purchase callbacks (`buyCloak` / `buyHat` `whenComplete`) | store and cosmetics screens on the render thread |
`getExternalUUIDAsString()` streams over `PLAYERS.keySet()` while other threads insert. That's a textbook `ConcurrentModificationException`, and a `HashMap` resize during a concurrent write can also corrupt the map. Iterating the cosmetic lists in the UI while a purchase completes has the same problem.
`session-java` runs notification handlers on a `CompletableFuture.runAsync` pool thread, so notifications always arrive off the main thread.
## Fix
Either:
- apply every state change on the client thread with `Providers.saturn.getClient().executeOnThread(...)`, as the player worker already does in one place, **or**
- switch to `ConcurrentHashMap` and `CopyOnWriteArrayList`. `ConcurrentHashMap` doesn't allow `null` values, so this goes together with the "not a Saturn user" marker from the `get_player` issue.
Moving writes onto the client thread is the more robust option, since the render code reads these without locks.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Several collections are written from background threads and read on the render thread, with no synchronisation:
SaturnPlayer.PLAYERS(HashMap)get→put(uuid, null)), auth thread (SaturnPlayer.set), the notification thread (playernotification →SaturnPlayer.set)getExternalUUIDAsString()Cloaks.availableCloaks,Hats.availableHats(ArrayList)authenticate), purchase callbacks (buyCloak/buyHatwhenComplete)getExternalUUIDAsString()streams overPLAYERS.keySet()while other threads insert. That's a textbookConcurrentModificationException, and aHashMapresize during a concurrent write can also corrupt the map. Iterating the cosmetic lists in the UI while a purchase completes has the same problem.session-javaruns notification handlers on aCompletableFuture.runAsyncpool thread, so notifications always arrive off the main thread.Fix
Either:
Providers.saturn.getClient().executeOnThread(...), as the player worker already does in one place, orConcurrentHashMapandCopyOnWriteArrayList.ConcurrentHashMapdoesn't allownullvalues, so this goes together with the "not a Saturn user" marker from theget_playerissue.Moving writes onto the client thread is the more robust option, since the render code reads these without locks.