Make player and island metadata maps thread-safe - #3082
Merged
Merged
Conversation
Players.getMetaData() tested whether the backing map was immutable by writing to it on every read. The map was a plain HashMap, so any concurrent access from another thread could corrupt it, after which keySet().iterator().next() threw NoSuchElementException on every read. Addons like Border read player metadata on every PlayerMoveEvent, so a corrupted map spams the console until restart. Players and Island now copy metadata into a ConcurrentHashMap once, on first access or on setMetaData, and never mutate on read. Immutable maps from deserialization are handled by the same copy. The Island copy constructor uses the same helper. putMetaData(key, null) now removes the key because a concurrent map cannot hold null values. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 19, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A server admin reported console spam on every
PlayerMoveEventwith BentoBox 3.22.3 and Border 4.8.5 (https://mclo.gs/h623Vin):Players.getMetaData()calledisImmutable(map)on every read, and that helper wrote to the map each time (it re-put the first existing key to see whether anUnsupportedOperationExceptionwas thrown). The map was a plainHashMapwith no synchronization, so every metadata read anywhere in the ecosystem was a concurrent-unsafe write. If any other thread touched the map at the same time, theHashMapcould end up withsize > 0but an empty bucket table. From then on!map.isEmpty()was true butkeySet().iterator().next()found nothing and threw, on every read, until restart.Fix
PlayersandIslandcopy metadata into aConcurrentHashMaponce, on first access or onsetMetaData, and never mutate on read. Theinstanceof ConcurrentMapcheck replaces the write-probe, and also covers immutable maps coming from deserialization.setMetaDatacopies the caller's map instead of adopting it, so immutable maps are accepted (previouslyPlayers.setMetaDatathrewIllegalArgumentException) and the caller's map is never mutated.Islandcopy constructor uses the same helper.MetaDataAble.putMetaData(key, null)now removes the key, because a concurrent map cannot hold null values. Null keys/values are dropped when copying.Tests
PlayersTestcovering lazy init, immutable-map copy, caller-map isolation, null handling, and a multi-threaded hammer test that exercised the old corruption path.IslandTestgains equivalent immutable-map, caller-isolation and copy-constructor tests.🤖 Generated with Claude Code