Prevent DuckDB entity interaction queue stalls (#1002)

This commit is contained in:
DarkChroma authored and GitHub committed 2026-09-19 12:37:10 -06:00
1 parent 9cf12ee5bf
commit 0ec76561dc
2 files changed
+92

No files matched your search

@@ -25,6 +25,7 @@ import net.coreprotect.consumer.Queue;
import net.coreprotect.database.ConsumerEntitySpawnUpdates;
import net.coreprotect.database.ConsumerWriteBatch;
import net.coreprotect.database.Database;
import net.coreprotect.database.DatabaseType;
import net.coreprotect.database.DuckDBRecovery;
import net.coreprotect.database.logger.EntityInteractionLogger;
import net.coreprotect.database.rollback.EntitySpawnRollbackHandler;
@@ -361,6 +362,12 @@ public class Process {
});
}
catch (Exception e) {
if (shouldDiscardFailedEvent(ConfigHandler.databaseType, action, e)) {
Database.acknowledgeRollbackOnlyTransaction();
cancelEntityInteractionPromotion(interaction);
ErrorReporter.report(new IllegalStateException("Dropped entity interaction after a duplicate DuckDB entity UUID prevented identity creation: " + interaction.getEntityUuid(), e));
break;
}
pendingEntityInteractions.add(new PendingEntityInteraction(user, interaction, false, true));
throw e;
}
@@ -1064,6 +1071,24 @@ public class Process {
return !sqlFailure;
}
static boolean shouldDiscardFailedEvent(DatabaseType databaseType, int action, Throwable failure) {
if (!databaseType.isDuckDB() || action != ENTITY_INTERACTION) {
return false;
}
Set<Throwable> visited = new HashSet<>();
while (failure != null && visited.add(failure)) {
String message = failure.getMessage();
if (failure instanceof SQLException && message != null
&& message.contains("Constraint Error: Duplicate key \"uuid: ")
&& message.contains("violates unique constraint")) {
return true;
}
failure = failure.getCause();
}
return false;
}
private static void clearConsumerData(int processId, ArrayList<Object[]> consumerData, Map<Integer, String[]> users, Map<Integer, Object> consumerObject) {
discardProcessedConsumerData(processId, consumerData, users, consumerObject, consumerData.size());
users.clear();
@@ -0,0 +1,67 @@
package net.coreprotect.consumer.process;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.sql.SQLException;
import org.junit.jupiter.api.Test;
import net.coreprotect.database.DatabaseType;
class ProcessTest {
private static final String DUPLICATE_UUID = "Constraint Error: Duplicate key \"uuid: 2114a8b0-4978-4191-a5e0-14ca05436e83\" violates unique constraint.";
@Test
void discardsDuckDbEntityInteractionBlockedByDuplicateUuid() {
SQLException failure = new SQLException(DUPLICATE_UUID);
assertTrue(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_INTERACTION, failure));
}
@Test
void findsDuplicateUuidInWrappedSqlFailure() {
Exception failure = new Exception("Unable to log entity interaction", new SQLException(DUPLICATE_UUID));
assertTrue(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_INTERACTION, failure));
}
@Test
void retainsTransientDuckDbEntityInteractionFailure() {
SQLException failure = new SQLException("Connection reset while inserting entity identity");
assertFalse(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_INTERACTION, failure));
}
@Test
void retainsDuplicateUuidFailureForOtherDatabase() {
SQLException failure = new SQLException(DUPLICATE_UUID);
assertFalse(Process.shouldDiscardFailedEvent(DatabaseType.SQLITE, Process.ENTITY_INTERACTION, failure));
}
@Test
void retainsDuplicateUuidFailureForOtherAction() {
SQLException failure = new SQLException(DUPLICATE_UUID);
assertFalse(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_SPAWN_LOG, failure));
}
@Test
void retainsDuckDbEntityInteractionFailureForDifferentUniqueKey() {
SQLException failure = new SQLException("Constraint Error: Duplicate key \"kill_rowid: 42\" violates unique constraint.");
assertFalse(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_INTERACTION, failure));
}
@Test
void stopsAtCyclicExceptionCauses() {
Exception first = new Exception("first");
Exception second = new Exception("second");
first.initCause(second);
second.initCause(first);
assertFalse(Process.shouldDiscardFailedEvent(DatabaseType.DUCKDB, Process.ENTITY_INTERACTION, first));
}
}