Make /co reload swap world configs and leave block groups alone - #1015
Merged
Intelli merged 4 commits intoOct 2, 2026
Merged
Conversation
The per-world config map was a plain HashMap that getConfig wrote into on a miss, and reload cleared it before refilling, so a listener hitting that window could leave a world on the global config until the next reload. The map is now concurrent and read-only for listeners, and reload swaps new configs in without a gap. Reload also stops rebuilding the version adapters and block groups, which only depend on the server version, while listeners read them.
❌ Deploy Preview for coreprotect failed. Why did it fail? →
|
Contributor
|
Thanks -- automated review is requesting the following changes:
|
Several block groups are filled from datapack tags (buttons, pressure plates, doors, saplings, flowers, carpets, logs and more), so they are not fixed by the server version. Running the adapters and BlockGroup.initialize() only at startup would leave those groups stale after a datapack reload until the server restarts. Restore the previous behaviour and keep this change to the world config map.
…ad-world-config-swap
…ad-world-config-swap
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.
Summary
Per-world configs live in a plain
HashMapthat listeners on many threads read, and thatConfig.getConfigalso writes to./co reloadclears it before refilling it. A listener that looks up its world in that window can overwrite the world's own config with the global one until the next reload. Reload also rebuilds the sharedBlockGroupsets in place while listeners read them. This change makes the map concurrent and read-only for listeners, swaps world configs in without a gap, and builds the block groups only at startup.The problem
Config.getConfig(config/Config.java:330-337) is called by nearly every listener, on region threads on Folia and on async threads everywhere. On a miss it writes intoCONFIG_BY_WORLD_NAME:CONFIG_BY_WORLD_NAMEis a plainHashMap, so this is an unsynchronizedputfrom several threads at once./co reloadrunsCONFIG_BY_WORLD_NAME.clear()(Config.java:491) and then puts each world config back. Between the clear and the put, a listener for a world that has its own config (plugins/CoreProtect/world_nether.yml) misses and putsGLOBALfor that world. If thatputlands after the reload has put the world's real config, the world keeps using the global config until the next/co reload. An admin who disabled, for example,item-transactionsin one world sees it logged again after a reload, with nothing in the console to say why.ConfigHandler.performInitializationre-runsBukkitAdapter.loadAdapter(),SpigotAdapter.loadAdapter(),PaperAdapter.loadAdapter()andBlockGroup.initialize()on every/co reload(config/ConfigHandler.java:916-919). The adapters replace some publicBlockGroupsets with newHashSets and add to others (Bukkit_v1_19.java:87,Bukkit_v1_20.java:75), andinitialize()adds to them again, while listeners on other threads iterate and test those same sets without a lock.The fix
CONFIG_BY_WORLD_NAMEis aConcurrentHashMap.getConfigonly reads:CONFIG_BY_WORLD_NAME.getOrDefault(worldName, GLOBAL). A world without its own config getsGLOBALas before, without writing it into the map.putAlls them and removes the worlds whose config file is gone. A configured world reads either its old config or its new one, never the global one in between.BlockGroup.initialize()run only whenperformInitialization(true)is called at startup. They depend only on the server version (ConfigHandler.SERVER_VERSION,isPaper,isSpigot), which cannot change during/co reload. Their config reads (HOVER_EVENTS,MYSQL) happen at call time, so a reload still takes effect for them.Behaviour change
None once a reload completes. During a reload, each world keeps its previous config until its new one is in place.
Risk
performInitialization(false)is only called fromReloadCommand, so plugin enable still builds everything.GLOBALitself is still reloaded in place, as before. That is a separate, narrower issue, and I have not touched it here.Testing
Build:
mvn packagepasses.Reload check, a small plugin (ReloadCheck) on Paper 26.2 and Folia 1.21.11 with SQLite: dispense and explode, write a world config with
item-transactions: false,/co reload, dispense and explode again, delete the world config,/co reload, dispense again. Upstream and this branch both logged 1 container row, then 0 with the world config, then 1 after it was removed, with the explosions logging the same 2 block rows each time and no errors. That shows reload still applies and removes world configs and that block groups still classify the explosion after a reload. It does not provoke the reload race itself.Row parity: the 47-step scenario on Paper 26.2 with SQLite matches upstream apart from the random plant that bone meal grows.
Suggested test: add
plugins/CoreProtect/<world>.ymlwithitem-transactions: false, run/co reload, fire a dispenser and check no container row is logged. Remove the file,/co reload, fire again and check the row is logged. Repeat the reload many times while a dispenser clock runs, and confirm the world config is never lost.