moved database access to a dedicated single-thread executor
SQLite I/O no longer runs on the main server thread and the shared PreparedStatements are confined to one thread. savePixelBlock captures a snapshot of the block data on the calling thread. Main.pixelBlocks is now a CopyOnWriteArrayList and list mutations moved out of async chains. Blocks that fail to initialize on startup are skipped with a proper error log instead of being registered half-built.
This commit is contained in:
@@ -15,10 +15,9 @@ import org.bukkit.event.Listener;
|
|||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public final class Main extends JavaPlugin {
|
public final class Main extends JavaPlugin {
|
||||||
@@ -28,7 +27,7 @@ public final class Main extends JavaPlugin {
|
|||||||
|
|
||||||
private static TaskChainFactory taskFactory;
|
private static TaskChainFactory taskFactory;
|
||||||
|
|
||||||
public final static List<PixelBlock> pixelBlocks = new ArrayList<>();
|
public final static List<PixelBlock> pixelBlocks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public static <T> TaskChain<T> sharedChain(String name) {
|
public static <T> TaskChain<T> sharedChain(String name) {
|
||||||
return taskFactory.newSharedChain(name);
|
return taskFactory.newSharedChain(name);
|
||||||
@@ -94,11 +93,7 @@ public final class Main extends JavaPlugin {
|
|||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
Bukkit.getOnlinePlayers().forEach(QuitWhileInPixelBlockListener::kickPlayerOutOfWorld);
|
Bukkit.getOnlinePlayers().forEach(QuitWhileInPixelBlockListener::kickPlayerOutOfWorld);
|
||||||
try {
|
database.close();
|
||||||
database.close();
|
|
||||||
} catch(SQLException e) {
|
|
||||||
throw new RuntimeException("Failed disabling", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Main plugin() {
|
public static Main plugin() {
|
||||||
|
|||||||
@@ -7,14 +7,38 @@ import org.bukkit.Location;
|
|||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public class PixelBlockDatabase {
|
public class PixelBlockDatabase {
|
||||||
private final Connection db;
|
private final Connection db;
|
||||||
|
|
||||||
|
// Alle Statement-Zugriffe nach dem Startup laufen ausschließlich auf diesem Thread,
|
||||||
|
// da die geteilten PreparedStatements nicht threadsicher sind.
|
||||||
|
private final ExecutorService executor =
|
||||||
|
Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "PixelBlocks-Database"));
|
||||||
|
|
||||||
private final PreparedStatement getAllPixelBlocks;
|
private final PreparedStatement getAllPixelBlocks;
|
||||||
private final PreparedStatement deletePixelBlock;
|
private final PreparedStatement deletePixelBlock;
|
||||||
private final PreparedStatement insertOrReplacePixelBlock;
|
private final PreparedStatement insertOrReplacePixelBlock;
|
||||||
|
|
||||||
|
private record PixelBlockRow(
|
||||||
|
String uuid,
|
||||||
|
String owner,
|
||||||
|
String worldName,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
String entryWorldName,
|
||||||
|
double entryX,
|
||||||
|
double entryY,
|
||||||
|
double entryZ,
|
||||||
|
String direction
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public PixelBlockDatabase(String url) {
|
public PixelBlockDatabase(String url) {
|
||||||
try {
|
try {
|
||||||
Class.forName("org.sqlite.JDBC");
|
Class.forName("org.sqlite.JDBC");
|
||||||
@@ -50,52 +74,78 @@ public class PixelBlockDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws SQLException {
|
public void close() {
|
||||||
deletePixelBlock.close();
|
this.executor.shutdown();
|
||||||
getAllPixelBlocks.close();
|
try {
|
||||||
insertOrReplacePixelBlock.close();
|
if(!this.executor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
db.close();
|
Main.logger().warning("Database executor did not terminate in time, forcing shutdown");
|
||||||
|
this.executor.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
this.executor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePixelBlock.close();
|
||||||
|
this.getAllPixelBlocks.close();
|
||||||
|
this.insertOrReplacePixelBlock.close();
|
||||||
|
this.db.close();
|
||||||
|
} catch(SQLException e) {
|
||||||
|
Main.logger().log(Level.SEVERE, "Failed closing the database", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deletePixelBlock(PixelBlock pixelBlock) {
|
public void deletePixelBlock(PixelBlock pixelBlock) {
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(Main.plugin(), () -> {
|
String uuid = pixelBlock.getBlockUUID().toString();
|
||||||
|
this.executor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
this.deletePixelBlock.setString(1, pixelBlock.getBlockUUID().toString());
|
this.deletePixelBlock.setString(1, uuid);
|
||||||
this.deletePixelBlock.executeUpdate();
|
this.deletePixelBlock.executeUpdate();
|
||||||
} catch(SQLException e) {
|
} catch(SQLException e) {
|
||||||
throw new RuntimeException("Failed to delete PixelBlock from the database", e);
|
Main.logger().log(Level.SEVERE, String.format("Failed to delete PixelBlock '%s' from the database", uuid), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void savePixelBlock(PixelBlock pixelBlock) {
|
public void savePixelBlock(PixelBlock pixelBlock) {
|
||||||
Bukkit.getScheduler().runTask(Main.plugin(), () -> {
|
Location blockLocation = pixelBlock.getPixelBlockLocation();
|
||||||
|
Location entryLocation = pixelBlock.hasLastEntryLocation() ? pixelBlock.getLastEntryLocation() : blockLocation;
|
||||||
|
|
||||||
|
PixelBlockRow row = new PixelBlockRow(
|
||||||
|
pixelBlock.getBlockUUID().toString(),
|
||||||
|
pixelBlock.getOwnerUUID().toString(),
|
||||||
|
blockLocation.getWorld().getName(),
|
||||||
|
blockLocation.getX(),
|
||||||
|
blockLocation.getY(),
|
||||||
|
blockLocation.getZ(),
|
||||||
|
entryLocation.getWorld().getName(),
|
||||||
|
entryLocation.getX(),
|
||||||
|
entryLocation.getY(),
|
||||||
|
entryLocation.getZ(),
|
||||||
|
pixelBlock.getFacingDirection().toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.executor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
this.insertOrReplacePixelBlock.setString(1, pixelBlock.getBlockUUID().toString());
|
this.insertOrReplacePixelBlock.setString(1, row.uuid());
|
||||||
this.insertOrReplacePixelBlock.setString(2, pixelBlock.getOwnerUUID().toString());
|
this.insertOrReplacePixelBlock.setString(2, row.owner());
|
||||||
|
|
||||||
this.insertOrReplacePixelBlock.setString(3, pixelBlock.getPixelBlockLocation().getWorld().getName());
|
this.insertOrReplacePixelBlock.setString(3, row.worldName());
|
||||||
this.insertOrReplacePixelBlock.setDouble(4, pixelBlock.getPixelBlockLocation().getX());
|
this.insertOrReplacePixelBlock.setDouble(4, row.x());
|
||||||
this.insertOrReplacePixelBlock.setDouble(5, pixelBlock.getPixelBlockLocation().getY());
|
this.insertOrReplacePixelBlock.setDouble(5, row.y());
|
||||||
this.insertOrReplacePixelBlock.setDouble(6, pixelBlock.getPixelBlockLocation().getZ());
|
this.insertOrReplacePixelBlock.setDouble(6, row.z());
|
||||||
|
|
||||||
if(pixelBlock.hasLastEntryLocation()) {
|
this.insertOrReplacePixelBlock.setString(7, row.entryWorldName());
|
||||||
this.insertOrReplacePixelBlock.setString(7, pixelBlock.getLastEntryLocation().getWorld().getName());
|
this.insertOrReplacePixelBlock.setDouble(8, row.entryX());
|
||||||
this.insertOrReplacePixelBlock.setDouble(8, pixelBlock.getLastEntryLocation().getX());
|
this.insertOrReplacePixelBlock.setDouble(9, row.entryY());
|
||||||
this.insertOrReplacePixelBlock.setDouble(9, pixelBlock.getLastEntryLocation().getY());
|
this.insertOrReplacePixelBlock.setDouble(10, row.entryZ());
|
||||||
this.insertOrReplacePixelBlock.setDouble(10, pixelBlock.getLastEntryLocation().getZ());
|
|
||||||
} else {
|
|
||||||
this.insertOrReplacePixelBlock.setString(7, pixelBlock.getPixelBlockLocation().getWorld().getName());
|
|
||||||
this.insertOrReplacePixelBlock.setDouble(8, pixelBlock.getPixelBlockLocation().getX());
|
|
||||||
this.insertOrReplacePixelBlock.setDouble(9, pixelBlock.getPixelBlockLocation().getY());
|
|
||||||
this.insertOrReplacePixelBlock.setDouble(10, pixelBlock.getPixelBlockLocation().getZ());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.insertOrReplacePixelBlock.setString(11, pixelBlock.getFacingDirection().toString());
|
this.insertOrReplacePixelBlock.setString(11, row.direction());
|
||||||
|
|
||||||
this.insertOrReplacePixelBlock.executeUpdate();
|
this.insertOrReplacePixelBlock.executeUpdate();
|
||||||
} catch(SQLException e) {
|
} catch(SQLException e) {
|
||||||
throw new RuntimeException("Failed to create or update PixelBlock in the database", e);
|
Main.logger().log(Level.SEVERE, String.format("Failed to create or update PixelBlock '%s' in the database", row.uuid()), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -105,27 +155,32 @@ public class PixelBlockDatabase {
|
|||||||
ResultSet allPixelBlocks = this.getAllPixelBlocks.executeQuery();
|
ResultSet allPixelBlocks = this.getAllPixelBlocks.executeQuery();
|
||||||
|
|
||||||
while(allPixelBlocks.next()) {
|
while(allPixelBlocks.next()) {
|
||||||
Location blockLocation = new Location(
|
String uuid = allPixelBlocks.getString("uuid");
|
||||||
Bukkit.getWorld(allPixelBlocks.getString("locationWorldName")),
|
try {
|
||||||
allPixelBlocks.getDouble("locationX"),
|
Location blockLocation = new Location(
|
||||||
allPixelBlocks.getDouble("locationY"),
|
Bukkit.getWorld(allPixelBlocks.getString("locationWorldName")),
|
||||||
allPixelBlocks.getDouble("locationZ")
|
allPixelBlocks.getDouble("locationX"),
|
||||||
);
|
allPixelBlocks.getDouble("locationY"),
|
||||||
|
allPixelBlocks.getDouble("locationZ")
|
||||||
|
);
|
||||||
|
|
||||||
Location entryLocation = new Location(
|
Location entryLocation = new Location(
|
||||||
Bukkit.getWorld(allPixelBlocks.getString("entryLocationWorldName")),
|
Bukkit.getWorld(allPixelBlocks.getString("entryLocationWorldName")),
|
||||||
allPixelBlocks.getDouble("entryLocationX"),
|
allPixelBlocks.getDouble("entryLocationX"),
|
||||||
allPixelBlocks.getDouble("entryLocationY"),
|
allPixelBlocks.getDouble("entryLocationY"),
|
||||||
allPixelBlocks.getDouble("entryLocationZ")
|
allPixelBlocks.getDouble("entryLocationZ")
|
||||||
);
|
);
|
||||||
|
|
||||||
Main.pixelBlocks.add(PixelBlock.fromExisting(
|
Main.pixelBlocks.add(PixelBlock.fromExisting(
|
||||||
UUID.fromString(allPixelBlocks.getString("uuid")),
|
UUID.fromString(uuid),
|
||||||
UUID.fromString(allPixelBlocks.getString("owner")),
|
UUID.fromString(allPixelBlocks.getString("owner")),
|
||||||
blockLocation,
|
blockLocation,
|
||||||
Direction.valueOf(allPixelBlocks.getString("direction")),
|
Direction.valueOf(allPixelBlocks.getString("direction")),
|
||||||
entryLocation
|
entryLocation
|
||||||
));
|
));
|
||||||
|
} catch(Exception e) {
|
||||||
|
Main.logger().log(Level.SEVERE, String.format("Failed initializing existing pixelblock '%s'", uuid), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(SQLException e) {
|
} catch(SQLException e) {
|
||||||
throw new RuntimeException("Failed loading PixelBlocks from the database", e);
|
throw new RuntimeException("Failed loading PixelBlocks from the database", e);
|
||||||
|
|||||||
@@ -118,12 +118,8 @@ public class PixelBlock {
|
|||||||
|
|
||||||
this.scheduleEntityUpdate();
|
this.scheduleEntityUpdate();
|
||||||
|
|
||||||
this.getBlockTaskChain()
|
Main.database().savePixelBlock(this);
|
||||||
.async(() -> {
|
Main.pixelBlocks.add(this);
|
||||||
Main.database().savePixelBlock(this);
|
|
||||||
Main.pixelBlocks.add(this);
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
this.getBlockTaskChain().sync(() -> this.isAccessible = true).execute();
|
this.getBlockTaskChain().sync(() -> this.isAccessible = true).execute();
|
||||||
}
|
}
|
||||||
@@ -139,9 +135,9 @@ public class PixelBlock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastEntryLocation = player.getLocation();
|
this.lastEntryLocation = player.getLocation();
|
||||||
|
Main.database().savePixelBlock(this);
|
||||||
|
|
||||||
getBlockTaskChain()
|
getBlockTaskChain()
|
||||||
.async(() -> Main.database().savePixelBlock(this))
|
|
||||||
.sync(() -> {
|
.sync(() -> {
|
||||||
if(!this.isAccessible) return;
|
if(!this.isAccessible) return;
|
||||||
player.teleport(this.pixelWorld.getSpawnLocation());
|
player.teleport(this.pixelWorld.getSpawnLocation());
|
||||||
@@ -204,11 +200,10 @@ public class PixelBlock {
|
|||||||
world.playSound(this.pixelBlockLocation, Sound.BLOCK_COPPER_BULB_BREAK, 1.0F, 30);
|
world.playSound(this.pixelBlockLocation, Sound.BLOCK_COPPER_BULB_BREAK, 1.0F, 30);
|
||||||
world.dropItem(this.pixelBlockLocation.add(new Vector(0.5, 0.5, 0.5)), PixelBlockItem.getBlockAsItem(this));
|
world.dropItem(this.pixelBlockLocation.add(new Vector(0.5, 0.5, 0.5)), PixelBlockItem.getBlockAsItem(this));
|
||||||
})
|
})
|
||||||
.async(() -> {
|
|
||||||
Main.database().deletePixelBlock(this);
|
|
||||||
Main.pixelBlocks.remove(this);
|
|
||||||
})
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
Main.database().deletePixelBlock(this);
|
||||||
|
Main.pixelBlocks.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeEntities() {
|
private void removeEntities() {
|
||||||
|
|||||||
Reference in New Issue
Block a user