From 4ac56774fb209b3679086abf89cf4e5322720ac3 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Tue, 2 Jun 2026 21:42:26 -0500 Subject: [PATCH] Add error messages if DB becomes corrupted --- .../core/sql/DbCorruptedException.java | 25 ++++++++++ .../core/sql/repo/AbstractDhRepo.java | 46 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/sql/DbCorruptedException.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/DbCorruptedException.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/DbCorruptedException.java new file mode 100644 index 000000000..5a211007a --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/DbCorruptedException.java @@ -0,0 +1,25 @@ +package com.seibel.distanthorizons.core.sql; + +import java.sql.SQLException; + +/** + * Used to simplify handling when a database is corrupted + * since Java doesn't have a specific exception to handle corrupted databases + */ +public class DbCorruptedException extends SQLException +{ + public DbCorruptedException(String message) { super(message); } + public DbCorruptedException(String message, Throwable cause) { super(message, cause); } + public DbCorruptedException(Throwable cause) { super(cause); } + + + // helper methods // + + public static boolean isCorruptedException(SQLException e) + { + String message = e.getMessage().toLowerCase(); + return message.contains("sqlite_corrupt") + || message.contains("malformed"); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java index 67f96ab08..659fa3122 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java @@ -19,10 +19,13 @@ package com.seibel.distanthorizons.core.sql.repo; +import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.enums.MinecraftTextFormat; import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DbConnectionClosedException; +import com.seibel.distanthorizons.core.sql.DbCorruptedException; import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; import com.seibel.distanthorizons.core.sql.repo.phantoms.AutoClosableTrackingWrapper; import com.seibel.distanthorizons.core.util.ExceptionUtil; @@ -37,6 +40,7 @@ import java.io.IOException; import java.sql.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; @@ -55,6 +59,7 @@ public abstract class AbstractDhRepo> implemen private static final ConcurrentHashMap CONNECTIONS_BY_CONNECTION_STRING = new ConcurrentHashMap<>(); private static final ConcurrentHashMap, String> ACTIVE_CONNECTION_STRINGS_BY_REPO = new ConcurrentHashMap<>(); + private static final Set CORRUPTED_DB_PATHS = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -70,6 +75,8 @@ public abstract class AbstractDhRepo> implemen protected final KeyedLockContainer saveLockContainer = new KeyedLockContainer<>(); + private final AtomicBoolean databaseCorruptedRef = new AtomicBoolean(false); + //=============// @@ -227,7 +234,6 @@ public abstract class AbstractDhRepo> implemen finally { saveLock.unlock(); - //this.tryTriggerWalFlush(); } } private void insert(TDTO dto) @@ -392,6 +398,13 @@ public abstract class AbstractDhRepo> implemen @Nullable public ResultSet query(@Nullable PreparedStatement statement) throws RuntimeException { + // if the DB is corrupted act as if it's closed + // that should prevent further harm + if (this.databaseCorruptedRef.get()) + { + return null; + } + // This is done so we don't have to add "if null" checks everywhere. // Normally this should only happen once the DB has been closed. if (statement == null) @@ -425,6 +438,33 @@ public abstract class AbstractDhRepo> implemen { return null; } + else if (DbCorruptedException.isCorruptedException(e)) + { + // error may trigger on multiple threads at once + synchronized (this) + { + // only trigger this error once per database + if (!this.databaseCorruptedRef.getAndSet(true) + // multiple repos may be open for the same path (client and server levels in singleplayer) + && CORRUPTED_DB_PATHS.add(this.databaseFile.getPath())) + { + LOGGER.error("DH database file at [" + this.databaseFile.getPath() + "] is corrupted. \n" + + "All operations to this DB are disabled, DH may behave strangely if you continue playing. \n" + + "Please leave the world and delete the corrupted database file to fix. \n" + + "Error: [" + e.getMessage() + "]", e); + + ClientApi.INSTANCE.showChatMessageNextFrame( + MinecraftTextFormat.DARK_RED + MinecraftTextFormat.BOLD + "DH database is corrupted." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" + + "DH will behave strangely if your continue playing. \n" + + "Please leave the world and delete the corrupted database file at: \n" + + MinecraftTextFormat.YELLOW + this.databaseFile.getPath() + MinecraftTextFormat.CLEAR_FORMATTING + "\n" + + "to resolve the issue. \n" + ); + } + } + + return null; + } else { String message = "Unexpected Query error: [" + e.getMessage() + "], for prepared statement: [" + statement + "]."; @@ -512,6 +552,10 @@ public abstract class AbstractDhRepo> implemen LOGGER.error("Unable to close the connection ["+connectionString+"], error: ["+e.getMessage()+"]"); } } + + + // clear the errors so they can be re-fired if needed + CORRUPTED_DB_PATHS.clear(); } @Override