Files
PixelBlocks/src/main/java/eu/mhsl/minecraft/pixelblocks/PixelBlockDatabase.java
T
MineTec bc43519077 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.
2026-07-23 22:57:22 +02:00

190 lines
7.5 KiB
Java

package eu.mhsl.minecraft.pixelblocks;
import eu.mhsl.minecraft.pixelblocks.pixelblock.PixelBlock;
import eu.mhsl.minecraft.pixelblocks.utils.Direction;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import java.sql.*;
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 {
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 deletePixelBlock;
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) {
try {
Class.forName("org.sqlite.JDBC");
this.db = DriverManager.getConnection(url);
this.db.createStatement().execute(
"CREATE TABLE IF NOT EXISTS pixelblocks (" +
"uuid CHAR(36) PRIMARY KEY, " +
"owner CHAR(36), " +
"locationWorldName CHAR(36), " +
"locationX DOUBLE, " +
"locationY DOUBLE, " +
"locationZ DOUBLE, " +
"entryLocationWorldName CHAR(36), " +
"entryLocationX DOUBLE, " +
"entryLocationY DOUBLE, " +
"entryLocationZ DOUBLE, " +
"direction CHAR(36)" +
")"
);
this.deletePixelBlock = this.db.prepareStatement("DELETE FROM pixelblocks WHERE uuid = ?");
this.getAllPixelBlocks = this.db.prepareStatement("SELECT * FROM pixelblocks");
this.insertOrReplacePixelBlock = this.db.prepareStatement(
"INSERT OR REPLACE INTO pixelblocks(uuid, owner, " +
"locationWorldName, locationX, locationY, locationZ, " +
"entryLocationWorldName, entryLocationX, entryLocationY, entryLocationZ, direction) " +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
);
} catch(SQLException | RuntimeException | ClassNotFoundException e) {
throw new RuntimeException("Error while initializing database", e);
}
}
public void close() {
this.executor.shutdown();
try {
if(!this.executor.awaitTermination(10, TimeUnit.SECONDS)) {
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) {
String uuid = pixelBlock.getBlockUUID().toString();
this.executor.execute(() -> {
try {
this.deletePixelBlock.setString(1, uuid);
this.deletePixelBlock.executeUpdate();
} catch(SQLException e) {
Main.logger().log(Level.SEVERE, String.format("Failed to delete PixelBlock '%s' from the database", uuid), e);
}
});
}
public void savePixelBlock(PixelBlock pixelBlock) {
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 {
this.insertOrReplacePixelBlock.setString(1, row.uuid());
this.insertOrReplacePixelBlock.setString(2, row.owner());
this.insertOrReplacePixelBlock.setString(3, row.worldName());
this.insertOrReplacePixelBlock.setDouble(4, row.x());
this.insertOrReplacePixelBlock.setDouble(5, row.y());
this.insertOrReplacePixelBlock.setDouble(6, row.z());
this.insertOrReplacePixelBlock.setString(7, row.entryWorldName());
this.insertOrReplacePixelBlock.setDouble(8, row.entryX());
this.insertOrReplacePixelBlock.setDouble(9, row.entryY());
this.insertOrReplacePixelBlock.setDouble(10, row.entryZ());
this.insertOrReplacePixelBlock.setString(11, row.direction());
this.insertOrReplacePixelBlock.executeUpdate();
} catch(SQLException e) {
Main.logger().log(Level.SEVERE, String.format("Failed to create or update PixelBlock '%s' in the database", row.uuid()), e);
}
});
}
public void loadPixelBlocks() {
try {
ResultSet allPixelBlocks = this.getAllPixelBlocks.executeQuery();
while(allPixelBlocks.next()) {
String uuid = allPixelBlocks.getString("uuid");
try {
Location blockLocation = new Location(
Bukkit.getWorld(allPixelBlocks.getString("locationWorldName")),
allPixelBlocks.getDouble("locationX"),
allPixelBlocks.getDouble("locationY"),
allPixelBlocks.getDouble("locationZ")
);
Location entryLocation = new Location(
Bukkit.getWorld(allPixelBlocks.getString("entryLocationWorldName")),
allPixelBlocks.getDouble("entryLocationX"),
allPixelBlocks.getDouble("entryLocationY"),
allPixelBlocks.getDouble("entryLocationZ")
);
Main.pixelBlocks.add(PixelBlock.fromExisting(
UUID.fromString(uuid),
UUID.fromString(allPixelBlocks.getString("owner")),
blockLocation,
Direction.valueOf(allPixelBlocks.getString("direction")),
entryLocation
));
} catch(Exception e) {
Main.logger().log(Level.SEVERE, String.format("Failed initializing existing pixelblock '%s'", uuid), e);
}
}
} catch(SQLException e) {
throw new RuntimeException("Failed loading PixelBlocks from the database", e);
}
}
}