From 60e74eff992c6e376c20ca9ae4d9cfa2347ac38e Mon Sep 17 00:00:00 2001 From: cmastudios Date: Sun, 6 Oct 2013 13:31:56 -0500 Subject: [PATCH] Load war zone blocks in batches to prevent server hangs War zone blocks are now loaded from the database at a configurable speed of blocks per tick. This prevents an entire server from hanging whenever a war zone is reset. The speed can be increased or decreased based on your server's performance. --- war/src/main/java/com/tommytony/war/War.java | 1 + .../main/java/com/tommytony/war/Warzone.java | 1 - .../com/tommytony/war/config/WarConfig.java | 3 +- .../war/job/PartialZoneResetJob.java | 85 +++++++++++++++++++ .../war/mapper/ZoneVolumeMapper.java | 6 +- .../com/tommytony/war/volume/ZoneVolume.java | 30 ++++++- war/src/main/resources/messages.properties | 1 + 7 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 war/src/main/java/com/tommytony/war/job/PartialZoneResetJob.java diff --git a/war/src/main/java/com/tommytony/war/War.java b/war/src/main/java/com/tommytony/war/War.java index a9faa51..25e48ee 100644 --- a/war/src/main/java/com/tommytony/war/War.java +++ b/war/src/main/java/com/tommytony/war/War.java @@ -183,6 +183,7 @@ public class War extends JavaPlugin { warConfig.put(WarConfig.MAXZONES, 12); warConfig.put(WarConfig.PVPINZONESONLY, false); warConfig.put(WarConfig.TNTINZONESONLY, false); + warConfig.put(WarConfig.RESETSPEED, 5000); warzoneDefaultConfig.put(WarzoneConfig.AUTOASSIGN, false); warzoneDefaultConfig.put(WarzoneConfig.BLOCKHEADS, true); diff --git a/war/src/main/java/com/tommytony/war/Warzone.java b/war/src/main/java/com/tommytony/war/Warzone.java index 1bf5272..18b8e43 100644 --- a/war/src/main/java/com/tommytony/war/Warzone.java +++ b/war/src/main/java/com/tommytony/war/Warzone.java @@ -1097,7 +1097,6 @@ public class Warzone { public void reinitialize() { this.isReinitializing = true; this.getVolume().resetBlocksAsJob(); - this.initializeZoneAsJob(); } public void handlePlayerLeave(Player player, Location destination, PlayerMoveEvent event, boolean removeFromTeam) { diff --git a/war/src/main/java/com/tommytony/war/config/WarConfig.java b/war/src/main/java/com/tommytony/war/config/WarConfig.java index fd14ce9..0101129 100644 --- a/war/src/main/java/com/tommytony/war/config/WarConfig.java +++ b/war/src/main/java/com/tommytony/war/config/WarConfig.java @@ -8,7 +8,8 @@ public enum WarConfig { KEEPOLDZONEVERSIONS (Boolean.class), MAXZONES (Integer.class), PVPINZONESONLY (Boolean.class), - TNTINZONESONLY (Boolean.class); + TNTINZONESONLY (Boolean.class), + RESETSPEED (Integer.class); private final Class configType; diff --git a/war/src/main/java/com/tommytony/war/job/PartialZoneResetJob.java b/war/src/main/java/com/tommytony/war/job/PartialZoneResetJob.java new file mode 100644 index 0000000..8fd9b9d --- /dev/null +++ b/war/src/main/java/com/tommytony/war/job/PartialZoneResetJob.java @@ -0,0 +1,85 @@ +package com.tommytony.war.job; + +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.logging.Level; + +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import com.tommytony.war.War; +import com.tommytony.war.Warzone; +import com.tommytony.war.structure.ZoneLobby; +import com.tommytony.war.volume.ZoneVolume; + +public class PartialZoneResetJob extends BukkitRunnable implements Cloneable { + + private final Warzone zone; + private final ZoneVolume volume; + private final int speed; + private final int total; + private int completed = 0; + private final long startTime = System.currentTimeMillis(); + private long messageCounter = System.currentTimeMillis(); + public static final long MESSAGE_INTERVAL = 3000; + // Ticks between job runs + public static final int JOB_INTERVAL = 1; + + /** + * Reset a warzone's blocks at a certain speed. + * + * @param volume + * Warzone to reset. + * @param speed + * Blocks to modify per #INTERVAL. + */ + public PartialZoneResetJob(Warzone zone, int speed) { + this.zone = zone; + this.volume = zone.getVolume(); + this.speed = speed; + this.total = volume.size(); + } + + @Override + public void run() { + try { + volume.resetSection(completed, speed); + completed += speed; + if (completed < total) { + if (System.currentTimeMillis() - messageCounter > MESSAGE_INTERVAL) { + messageCounter = System.currentTimeMillis(); + int percent = (int) (((double) completed / (double) total) * 100); + long seconds = (System.currentTimeMillis() - startTime) / 1000; + String message = MessageFormat.format( + War.war.getString("zone.battle.resetprogress"), + percent, seconds); + for (Player player : War.war.getServer().getOnlinePlayers()) { + ZoneLobby lobby = ZoneLobby.getLobbyByLocation(player); + if (zone.getPlayers().contains(player) + || (lobby != null && lobby.getZone() == zone)) { + War.war.msg(player, message); + } + } + } + War.war.getServer().getScheduler() + .runTaskLater(War.war, this.clone(), JOB_INTERVAL); + } else { + zone.initializeZone(); + War.war.getLogger().info( + "Finished reset cycle for warzone " + volume.getName()); + } + } catch (SQLException e) { + War.war.getLogger().log(Level.WARNING, + "Failed to load zone during reset loop", e); + } + } + + @Override + protected PartialZoneResetJob clone() { + try { + return (PartialZoneResetJob) super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(e); + } + } +} diff --git a/war/src/main/java/com/tommytony/war/mapper/ZoneVolumeMapper.java b/war/src/main/java/com/tommytony/war/mapper/ZoneVolumeMapper.java index 1ffb901..3db04b1 100644 --- a/war/src/main/java/com/tommytony/war/mapper/ZoneVolumeMapper.java +++ b/war/src/main/java/com/tommytony/war/mapper/ZoneVolumeMapper.java @@ -56,10 +56,12 @@ public class ZoneVolumeMapper { * @param String zoneName Zone to load the volume from * @param World world The world the zone is located * @param boolean onlyLoadCorners Should only the corners be loaded + * @param start Starting position to load blocks at + * @param total Amount of blocks to read * @return integer Changed blocks * @throws SQLException Error communicating with SQLite3 database */ - public static int load(ZoneVolume volume, String zoneName, World world, boolean onlyLoadCorners) throws SQLException { + public static int load(ZoneVolume volume, String zoneName, World world, boolean onlyLoadCorners, int start, int total) throws SQLException { int changed = 0; File databaseFile = new File(War.war.getDataFolder(), String.format("/dat/warzone-%s/volume-%s.sl3", zoneName, volume.getName())); if (!databaseFile.exists()) { @@ -99,7 +101,7 @@ public class ZoneVolumeMapper { databaseConnection.close(); return 0; } - ResultSet query = stmt.executeQuery("SELECT * FROM blocks"); + ResultSet query = stmt.executeQuery("SELECT * FROM blocks ORDER BY rowid LIMIT " + start + ", " + total); while (query.next()) { int x = query.getInt("x"), y = query.getInt("y"), z = query.getInt("z"); BlockState modify = corner1.getRelative(x, y, z).getState(); diff --git a/war/src/main/java/com/tommytony/war/volume/ZoneVolume.java b/war/src/main/java/com/tommytony/war/volume/ZoneVolume.java index 6a28fce..95d77f3 100644 --- a/war/src/main/java/com/tommytony/war/volume/ZoneVolume.java +++ b/war/src/main/java/com/tommytony/war/volume/ZoneVolume.java @@ -10,6 +10,8 @@ import org.bukkit.block.Block; import com.tommytony.war.Team; import com.tommytony.war.War; import com.tommytony.war.Warzone; +import com.tommytony.war.config.WarConfig; +import com.tommytony.war.job.PartialZoneResetJob; import com.tommytony.war.mapper.ZoneVolumeMapper; import com.tommytony.war.structure.Monument; @@ -48,7 +50,7 @@ public class ZoneVolume extends Volume { } public void loadCorners() throws SQLException { - ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), true); + ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), true, 0, 0); this.isSaved = true; } @@ -57,7 +59,7 @@ public class ZoneVolume extends Volume { // Load blocks directly from disk and onto the map (i.e. no more in-memory warzone blocks) int reset = 0; try { - reset = ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false); + reset = ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, 0, Integer.MAX_VALUE); } catch (SQLException ex) { War.war.log("Failed to load warzone " + zone.getName() + ": " + ex.getMessage(), Level.WARNING); ex.printStackTrace(); @@ -66,6 +68,30 @@ public class ZoneVolume extends Volume { this.isSaved = true; } + /** + * Reset a section of blocks in the warzone. + * + * @param start + * Starting position for reset. + * @param total + * Amount of blocks to reset. + * @throws SQLException + */ + public void resetSection(int start, int total) throws SQLException { + ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, start, total); + } + + @Override + /** + * Reset the blocks in this warzone at the speed defined in WarConfig#RESETSPEED. + * The job will automatically spawn new instances of itself to run every tick until it is done resetting all blocks. + */ + public void resetBlocksAsJob() { + PartialZoneResetJob job = new PartialZoneResetJob(zone, War.war + .getWarConfig().getInt(WarConfig.RESETSPEED)); + War.war.getServer().getScheduler().runTask(War.war, job); + } + public void setNorthwest(Location block) throws NotNorthwestException, TooSmallException, TooBigException { // northwest defaults to top block Location topBlock = new Location(block.getWorld(), block.getX(), block.getWorld().getMaxHeight(), block.getZ()); diff --git a/war/src/main/resources/messages.properties b/war/src/main/resources/messages.properties index 79159a0..d81c23a 100644 --- a/war/src/main/resources/messages.properties +++ b/war/src/main/resources/messages.properties @@ -92,6 +92,7 @@ zone.battle.end = The battle is over. Team {0} lost: {1} died and the zone.battle.newscores = New scores - {0} zone.battle.next = The battle was interrupted. Resetting warzone {0}... zone.battle.reset = A new battle begins. Resetting warzone... +zone.battle.resetprogress = Reset progress: {0}%, {1} seconds zone.bomb.broadcast = {0} blew up team {1}''s spawn. Team {2} scores one point. zone.cake.broadcast = {0} captured cake {1}. Team {2} scores one point and gets a full lifepool. zone.flagcapture.broadcast = {0} captured team {1}''s flag. Team {2} scores one point.