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:
2026-07-23 22:57:22 +02:00
parent ec74d5d8f2
commit bc43519077
3 changed files with 111 additions and 66 deletions
@@ -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() {