-
Notifications
You must be signed in to change notification settings - Fork 0
Add CivScheduler facade and migrate civmodcore sites #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,25 +3,27 @@ | |
| import java.util.Iterator; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
| import java.util.TreeMap; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.function.BiFunction; | ||
| import org.bukkit.Bukkit; | ||
| import org.bukkit.entity.Player; | ||
| import org.bukkit.scheduler.BukkitRunnable; | ||
| import vg.civcraft.mc.civmodcore.CivModCorePlugin; | ||
| import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; | ||
| import vg.civcraft.mc.civmodcore.scheduling.CivTask; | ||
|
|
||
| public class BottomLine implements Comparable<BottomLine> { | ||
|
|
||
| private Map<UUID, String> texts; | ||
| private String identifier; | ||
| private BukkitRunnable updater; | ||
| private CivTask updater; | ||
| private int priority; | ||
|
|
||
| BottomLine(String identifier, int priority) { | ||
| this.identifier = identifier; | ||
| this.priority = priority; | ||
| this.texts = new TreeMap<>(); | ||
| // Mutated by event handlers on region/entity threads while the updatePeriodically task iterates it | ||
| // on the global thread under Folia; UUID keys are never sorted, so a hash map is enough. | ||
| this.texts = new ConcurrentHashMap<>(); | ||
| } | ||
|
|
||
| public String getIdentifier() { | ||
|
|
@@ -44,30 +46,25 @@ public void updatePeriodically(BiFunction<Player, String, String> updateFunction | |
| if (updater != null) { | ||
| updater.cancel(); | ||
| } | ||
| updater = new BukkitRunnable() { | ||
|
|
||
| @Override | ||
| public void run() { | ||
| Iterator<Entry<UUID, String>> iter = texts.entrySet().iterator(); | ||
| while (iter.hasNext()) { | ||
| Entry<UUID, String> entry = iter.next(); | ||
| Player player = Bukkit.getPlayer(entry.getKey()); | ||
| if (player != null) { | ||
| String newText = updateFunction.apply(player, entry.getValue()); | ||
| if (newText == null) { | ||
| iter.remove(); | ||
| BottomLineAPI.refreshIndividually(player.getUniqueId()); | ||
| continue; | ||
| } | ||
| if (!newText.equals(entry.getValue())) { | ||
| entry.setValue(newText); | ||
| BottomLineAPI.refreshIndividually(player.getUniqueId()); | ||
| } | ||
| updater = CivScheduler.runGlobalTimer(() -> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Folia, player data updates and removals occur on region-specific threads, while this periodic updater runs on the global region thread. Since Please change this.texts = new ConcurrentHashMap<>();(Note: Since
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied. texts is now a ConcurrentHashMap. The map is written by updatePlayer/removePlayer from event handlers (citadel/bastion ModeListener, finale cooldown listeners, finale AsyncPacketHandler) which run on region/entity/async threads under Folia, while updatePeriodically's task iterates and structurally modifies it on the global thread via getGlobalRegionScheduler — a real cross-thread race on a plain TreeMap. UUID keys are non-null and no Navigable/sorted-order ops are used (refreshAll copies into its own separate map), so ConcurrentHashMap suffices over ConcurrentSkipListMap. |
||
| Iterator<Entry<UUID, String>> iter = texts.entrySet().iterator(); | ||
| while (iter.hasNext()) { | ||
| Entry<UUID, String> entry = iter.next(); | ||
| Player player = Bukkit.getPlayer(entry.getKey()); | ||
| if (player != null) { | ||
| String newText = updateFunction.apply(player, entry.getValue()); | ||
| if (newText == null) { | ||
| iter.remove(); | ||
| BottomLineAPI.refreshIndividually(player.getUniqueId()); | ||
| continue; | ||
| } | ||
| if (!newText.equals(entry.getValue())) { | ||
| entry.setValue(newText); | ||
| BottomLineAPI.refreshIndividually(player.getUniqueId()); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| updater.runTaskTimer(CivModCorePlugin.getInstance(), delay, delay); | ||
| }, delay, delay); | ||
| } | ||
|
|
||
| public void removePlayer(Player player) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,27 +3,28 @@ | |
| import java.util.Iterator; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
| import java.util.TreeMap; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.function.BiFunction; | ||
| import org.bukkit.Bukkit; | ||
| import org.bukkit.entity.Player; | ||
| import org.bukkit.scheduler.BukkitRunnable; | ||
| import org.bukkit.scoreboard.DisplaySlot; | ||
| import org.bukkit.scoreboard.Objective; | ||
| import org.bukkit.scoreboard.Score; | ||
| import org.bukkit.scoreboard.Scoreboard; | ||
| import vg.civcraft.mc.civmodcore.CivModCorePlugin; | ||
| import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; | ||
| import vg.civcraft.mc.civmodcore.scheduling.CivTask; | ||
|
|
||
| public class CivScoreBoard { | ||
|
|
||
| private String scoreName; | ||
| private Map<UUID, String> currentScoreText; | ||
| private BukkitRunnable updater; | ||
| private CivTask updater; | ||
|
|
||
| CivScoreBoard(String scoreName) { | ||
| this.scoreName = scoreName; | ||
| this.currentScoreText = new TreeMap<>(); | ||
| // Updater runs on the global region thread while set/hide/purge mutate from player region threads on Folia. | ||
| this.currentScoreText = new ConcurrentHashMap<>(); | ||
| } | ||
|
|
||
| public String getName() { | ||
|
|
@@ -34,30 +35,25 @@ public void updatePeriodically(BiFunction<Player, String, String> updateFunction | |
| if (updater != null) { | ||
| updater.cancel(); | ||
| } | ||
| updater = new BukkitRunnable() { | ||
|
|
||
| @Override | ||
| public void run() { | ||
| Iterator<Entry<UUID, String>> iter = currentScoreText.entrySet().iterator(); | ||
| while (iter.hasNext()) { | ||
| Entry<UUID, String> entry = iter.next(); | ||
| Player player = Bukkit.getPlayer(entry.getKey()); | ||
| if (player != null) { | ||
| String newText = updateFunction.apply(player, entry.getValue()); | ||
| if (newText == null) { | ||
| hideForPlayer(player); | ||
| iter.remove(); | ||
| continue; | ||
| } | ||
| if (!newText.equals(entry.getValue())) { | ||
| internalUpdate(player, entry.getValue(), newText); | ||
| entry.setValue(newText); | ||
| } | ||
| updater = CivScheduler.runGlobalTimer(() -> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Folia, scoreboard updates (like Please change this.currentScoreText = new ConcurrentHashMap<>();(Note: Since
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed and applied. currentScoreText is keyed by UUID (always p.getUniqueId(), non-null) and the code uses no Navigable/sorted-order operations, so ConcurrentHashMap is the right fit. The race is real on Folia: the migrated updatePeriodically now uses CivScheduler.runGlobalTimer (global region thread) and is live in production (finale CooldownHandler/PearlCoolDownListener), while set/hide are invoked from PlayerSetting.setValue listener callbacks and event-driven HUD updates that run on player region/entity threads — concurrent mutation of a plain TreeMap during iteration. Swapped TreeMap -> ConcurrentHashMap; its entrySet iterator still supports iter.remove() and entry.setValue() used by the updater. |
||
| Iterator<Entry<UUID, String>> iter = currentScoreText.entrySet().iterator(); | ||
| while (iter.hasNext()) { | ||
| Entry<UUID, String> entry = iter.next(); | ||
| Player player = Bukkit.getPlayer(entry.getKey()); | ||
| if (player != null) { | ||
| String newText = updateFunction.apply(player, entry.getValue()); | ||
| if (newText == null) { | ||
| hideForPlayer(player); | ||
| iter.remove(); | ||
| continue; | ||
| } | ||
| if (!newText.equals(entry.getValue())) { | ||
| internalUpdate(player, entry.getValue(), newText); | ||
| entry.setValue(newText); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| updater.runTaskTimer(CivModCorePlugin.getInstance(), delay, delay); | ||
| }, delay, delay); | ||
| } | ||
|
|
||
| public void set(Player p, String newText) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Folia, player login events and reads via
getPlayerNames()can occur concurrently across different region threads. Sincenamesis a non-thread-safeHashSet, modifying it here on the global region thread while other threads read or write to it will cause race conditions.Please change
namesto a thread-safe set implementation, such asConcurrentHashMap.newKeySet():There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed real under Folia: the seed task writes via runGlobal on the global region thread, onLogin (PlayerLoginEvent, MONITOR) writes from the connection/login thread, and the public static getPlayerNames() is read from arbitrary threads. Replaced the HashSet with ConcurrentHashMap.newKeySet(). Set semantics are unchanged (the original was unordered, so no Navigable/sorted ops to preserve) and the names are non-null player-name Strings, so newKeySet is the correct choice over ConcurrentSkipListMap. Collections.unmodifiableSet still gives a thread-safe live view.