Better exception handling for DataExtension gathering:

- Any method that causes an exception is temporarily disabled until
  next Plan reload
  - Gathering is attempted again for the rest of the methods of that
    extension
- Exceptions are properly logged instead of just names of the exceptions
This commit is contained in:
Rsl1122 2019-05-02 17:00:30 +03:00
parent 41720baf78
commit 6e78e9e7ad
11 changed files with 170 additions and 87 deletions

View File

@ -0,0 +1,44 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.api.exceptions;
import com.djrapitops.plan.extension.implementation.providers.MethodWrapper;
/**
* Exception that is thrown when a call to a DataExtension method throws an exception.
*
* @author Rsl1122
*/
public class DataExtensionMethodCallException extends IllegalStateException {
private final String pluginName;
private final MethodWrapper method;
public DataExtensionMethodCallException(Throwable cause, String pluginName, MethodWrapper method) {
super(cause);
this.pluginName = pluginName;
this.method = method;
}
public String getPluginName() {
return pluginName;
}
public MethodWrapper getMethod() {
return method;
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.data.plugin.PluginsConfigSection;
import com.djrapitops.plan.extension.implementation.CallerImplementation;
import com.djrapitops.plan.extension.implementation.DataProviderExtractor;
@ -111,7 +112,6 @@ public class ExtensionServiceImplementation implements ExtensionService {
if (extensionGatherers.remove(pluginName) != null) {
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, pluginName + " extension unregistered.");
}
}
private boolean shouldNotAllowRegistration(String pluginName) {
@ -150,16 +150,29 @@ public class ExtensionServiceImplementation implements ExtensionService {
gatherer.updateValues(playerUUID, playerName);
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, "Gathering completed: " + playerName);
} catch (Exception | NoClassDefFoundError | NoSuchMethodError | NoSuchFieldError e) {
logger.warn(gatherer.getPluginName() + " ran into (but failed safely) " + e.getClass().getSimpleName() +
" when updating value for '" + playerName +
"', (You can disable integration with setting 'Plugins." + gatherer.getPluginName() + ".Enabled')" +
" reason: '" + e.getMessage() +
} catch (DataExtensionMethodCallException methodCallFailed) {
logFailure(playerName, methodCallFailed);
gatherer.disableMethodFromUse(methodCallFailed.getMethod());
// Try again
updatePlayerValues(gatherer, playerUUID, playerName, event);
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError unexpectedError) {
logger.warn(gatherer.getPluginName() + " ran into unexpected error (please report this)" + unexpectedError +
" (but failed safely) when updating value for '" + playerName +
"', stack trace to follow:");
errorHandler.log(L.WARN, gatherer.getClass(), e);
errorHandler.log(L.WARN, gatherer.getClass(), unexpectedError);
}
}
private void logFailure(String playerName, DataExtensionMethodCallException methodCallFailed) {
Throwable cause = methodCallFailed.getCause();
String causeName = cause.getClass().getSimpleName();
logger.warn(methodCallFailed.getPluginName() + " ran into " + causeName +
" (but failed safely) when updating value for '" + playerName +
"', the method was disabled temporarily (won't be called until next Plan reload)" +
", stack trace to follow:");
errorHandler.log(L.WARN, getClass(), cause);
}
public void updateServerValues(CallEvents event) {
for (ProviderValueGatherer gatherer : extensionGatherers.values()) {
updateServerValues(gatherer, event);
@ -176,13 +189,15 @@ public class ExtensionServiceImplementation implements ExtensionService {
gatherer.updateValues();
logger.getDebugLogger().logOn(DebugChannels.DATA_EXTENSIONS, "Gathering completed for server");
} catch (Exception | NoClassDefFoundError | NoSuchMethodError | NoSuchFieldError e) {
logger.warn(gatherer.getPluginName() + " ran into (but failed safely) " + e.getClass().getSimpleName() +
" when updating value for server" +
", (You can disable integration with setting 'Plugins." + gatherer.getPluginName() + ".Enabled')" +
" reason: '" + e.getMessage() +
"', stack trace to follow:");
errorHandler.log(L.WARN, gatherer.getClass(), e);
} catch (DataExtensionMethodCallException methodCallFailed) {
logFailure("server", methodCallFailed);
gatherer.disableMethodFromUse(methodCallFailed.getMethod());
// Try again
updateServerValues(gatherer, event);
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError unexpectedError) {
logger.warn(gatherer.getPluginName() + " ran into unexpected error (please report this)" + unexpectedError +
" (but failed safely) when updating value for server, stack trace to follow:");
errorHandler.log(L.WARN, gatherer.getClass(), unexpectedError);
}
}
}

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan.extension.implementation.providers;
import com.djrapitops.plan.extension.implementation.MethodType;
import java.util.*;
import java.util.stream.Collectors;
/**
* Group class for handling multiple different types of {@link DataProvider}s.
@ -77,4 +78,24 @@ public class DataProviders {
}
return byReturnType;
}
public void removeProviderWithMethod(MethodWrapper method) {
MethodType methodType = method.getMethodType();
Map<Class, List<DataProvider>> byResultType = byMethodType.getOrDefault(methodType, Collections.emptyMap());
if (byResultType.isEmpty()) {
return;
}
Class resultType = method.getResultType();
List<DataProvider> providers = byResultType.getOrDefault(resultType, Collections.emptyList());
if (providers.isEmpty()) {
return;
}
byResultType.put(resultType, providers.stream()
.filter(provider -> provider.getMethod().equals(method))
.collect(Collectors.toList())
);
byMethodType.put(methodType, byResultType);
}
}

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.extension.implementation.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.UUID;
/**
@ -95,4 +96,19 @@ public class MethodWrapper<T> {
public Class<T> getResultType() {
return resultType;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MethodWrapper)) return false;
MethodWrapper<?> that = (MethodWrapper<?>) o;
return method.equals(that.method) &&
resultType.equals(that.resultType) &&
methodType == that.methodType;
}
@Override
public int hashCode() {
return Objects.hash(method, resultType, methodType);
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension.implementation.providers.gathering;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.extension.DataExtension;
@ -28,7 +29,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIc
import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreBooleanProviderTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StorePlayerBooleanResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerBooleanResultTransaction;
import com.djrapitops.plugin.logging.console.PluginLogger;
import java.util.*;
import java.util.concurrent.Callable;
@ -48,20 +48,17 @@ class BooleanProviderValueGatherer {
private final Database database;
private final DataProviders dataProviders;
private final PluginLogger logger;
BooleanProviderValueGatherer(
String pluginName, DataExtension extension,
UUID serverUUID, Database database,
DataProviders dataProviders,
PluginLogger logger
DataProviders dataProviders
) {
this.pluginName = pluginName;
this.extension = extension;
this.serverUUID = serverUUID;
this.database = database;
this.dataProviders = dataProviders;
this.logger = logger;
}
Conditions gatherBooleanDataOfPlayer(UUID playerUUID, String playerName) {
@ -127,10 +124,7 @@ class BooleanProviderValueGatherer {
boolean hidden = BooleanDataProvider.isHidden(booleanProvider);
MethodWrapper<Boolean> method = booleanProvider.getMethod();
Boolean result = getMethodResult(
methodCaller.apply(method),
throwable -> pluginName + " has invalid implementation, method " + method.getMethodName() + " threw exception: " + throwable.toString()
);
Boolean result = getMethodResult(methodCaller.apply(method), method);
if (result == null) {
// Error during method call
satisfied.add(booleanProvider); // Prevents further attempts to call this provider for this player.
@ -155,13 +149,11 @@ class BooleanProviderValueGatherer {
return satisfied;
}
private <T> T getMethodResult(Callable<T> callable, Function<Throwable, String> errorMsg) {
private <T> T getMethodResult(Callable<T> callable, MethodWrapper<T> method) {
try {
return callable.call();
} catch (Exception | NoSuchFieldError | NoSuchMethodError e) {
logger.warn(errorMsg.apply(e));
return null;
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
throw new DataExtensionMethodCallException(e, pluginName, method);
}
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension.implementation.providers.gathering;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.extension.DataExtension;
@ -30,7 +31,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.results
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StorePlayerPercentageResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerDoubleResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerPercentageResultTransaction;
import com.djrapitops.plugin.logging.console.PluginLogger;
import java.util.Optional;
import java.util.UUID;
@ -51,20 +51,17 @@ class DoubleAndPercentageProviderValueGatherer {
private final Database database;
private final DataProviders dataProviders;
private final PluginLogger logger;
DoubleAndPercentageProviderValueGatherer(
String pluginName, DataExtension extension,
UUID serverUUID, Database database,
DataProviders dataProviders,
PluginLogger logger
DataProviders dataProviders
) {
this.pluginName = pluginName;
this.extension = extension;
this.serverUUID = serverUUID;
this.database = database;
this.dataProviders = dataProviders;
this.logger = logger;
}
void gatherDoubleDataOfPlayer(UUID playerUUID, String playerName, Conditions conditions) {
@ -105,10 +102,7 @@ class DoubleAndPercentageProviderValueGatherer {
}
MethodWrapper<Double> method = doubleProvider.getMethod();
Double result = getMethodResult(
methodCaller.apply(method),
throwable -> pluginName + " has invalid implementation, method " + method.getMethodName() + " threw exception: " + throwable.toString()
);
Double result = getMethodResult(methodCaller.apply(method), method);
if (result == null) {
return;
}
@ -123,13 +117,11 @@ class DoubleAndPercentageProviderValueGatherer {
}
}
private <T> T getMethodResult(Callable<T> callable, Function<Throwable, String> errorMsg) {
private <T> T getMethodResult(Callable<T> callable, MethodWrapper<T> method) {
try {
return callable.call();
} catch (Exception | NoSuchFieldError | NoSuchMethodError e) {
logger.warn(errorMsg.apply(e));
return null;
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
throw new DataExtensionMethodCallException(e, pluginName, method);
}
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension.implementation.providers.gathering;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.extension.DataExtension;
@ -29,7 +30,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIc
import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreNumberProviderTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StorePlayerNumberResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerNumberResultTransaction;
import com.djrapitops.plugin.logging.console.PluginLogger;
import java.util.Optional;
import java.util.UUID;
@ -50,20 +50,17 @@ class NumberProviderValueGatherer {
private final Database database;
private final DataProviders dataProviders;
private final PluginLogger logger;
NumberProviderValueGatherer(
String pluginName, DataExtension extension,
UUID serverUUID, Database database,
DataProviders dataProviders,
PluginLogger logger
DataProviders dataProviders
) {
this.pluginName = pluginName;
this.extension = extension;
this.serverUUID = serverUUID;
this.database = database;
this.dataProviders = dataProviders;
this.logger = logger;
}
void gatherNumberDataOfPlayer(UUID playerUUID, String playerName, Conditions conditions) {
@ -100,10 +97,7 @@ class NumberProviderValueGatherer {
}
MethodWrapper<Long> method = numberProvider.getMethod();
Long result = getMethodResult(
methodCaller.apply(method),
throwable -> pluginName + " has invalid implementation, method " + method.getMethodName() + " threw exception: " + throwable.toString()
);
Long result = getMethodResult(methodCaller.apply(method), method);
if (result == null) {
return;
}
@ -115,12 +109,11 @@ class NumberProviderValueGatherer {
database.executeTransaction(storeTransactionCreator.apply(method, result));
}
private <T> T getMethodResult(Callable<T> callable, Function<Throwable, String> errorMsg) {
private <T> T getMethodResult(Callable<T> callable, MethodWrapper<T> method) {
try {
return callable.call();
} catch (Exception | NoSuchFieldError | NoSuchMethodError e) {
logger.warn(errorMsg.apply(e));
return null;
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
throw new DataExtensionMethodCallException(e, pluginName, method);
}
}
}

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.extension.icon.Icon;
import com.djrapitops.plan.extension.implementation.DataProviderExtractor;
import com.djrapitops.plan.extension.implementation.TabInformation;
import com.djrapitops.plan.extension.implementation.providers.DataProviders;
import com.djrapitops.plan.extension.implementation.providers.MethodWrapper;
import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIconTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.StorePluginTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.StoreTabInformationTransaction;
@ -44,12 +45,15 @@ public class ProviderValueGatherer {
private final DataProviderExtractor extractor;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private DataProviders dataProviders;
private BooleanProviderValueGatherer booleanGatherer;
private NumberProviderValueGatherer numberGatherer;
private DoubleAndPercentageProviderValueGatherer doubleAndPercentageGatherer;
private StringProviderValueGatherer stringGatherer;
private TableProviderValueGatherer tableGatherer;
public ProviderValueGatherer(
DataExtension extension,
DataProviderExtractor extractor,
@ -65,24 +69,28 @@ public class ProviderValueGatherer {
String pluginName = extractor.getPluginName();
UUID serverUUID = serverInfo.getServerUUID();
Database database = dbSystem.getDatabase();
DataProviders dataProviders = extractor.getDataProviders();
dataProviders = extractor.getDataProviders();
booleanGatherer = new BooleanProviderValueGatherer(
pluginName, extension, serverUUID, database, dataProviders, logger
pluginName, extension, serverUUID, database, dataProviders
);
numberGatherer = new NumberProviderValueGatherer(
pluginName, extension, serverUUID, database, dataProviders, logger
pluginName, extension, serverUUID, database, dataProviders
);
doubleAndPercentageGatherer = new DoubleAndPercentageProviderValueGatherer(
pluginName, extension, serverUUID, database, dataProviders, logger
pluginName, extension, serverUUID, database, dataProviders
);
stringGatherer = new StringProviderValueGatherer(
pluginName, extension, serverUUID, database, dataProviders, logger
pluginName, extension, serverUUID, database, dataProviders
);
tableGatherer = new TableProviderValueGatherer(
pluginName, extension, serverUUID, database, dataProviders, logger
pluginName, extension, serverUUID, database, dataProviders
);
}
public void disableMethodFromUse(MethodWrapper method) {
dataProviders.removeProviderWithMethod(method);
}
public boolean canCallEvent(CallEvents event) {
if (event == CallEvents.MANUAL) {
return true;

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension.implementation.providers.gathering;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.extension.DataExtension;
@ -28,7 +29,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIc
import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreStringProviderTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StorePlayerStringResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerStringResultTransaction;
import com.djrapitops.plugin.logging.console.PluginLogger;
import org.apache.commons.lang3.StringUtils;
import java.util.Optional;
@ -50,20 +50,17 @@ class StringProviderValueGatherer {
private final Database database;
private final DataProviders dataProviders;
private final PluginLogger logger;
StringProviderValueGatherer(
String pluginName, DataExtension extension,
UUID serverUUID, Database database,
DataProviders dataProviders,
PluginLogger logger
DataProviders dataProviders
) {
this.pluginName = pluginName;
this.extension = extension;
this.serverUUID = serverUUID;
this.database = database;
this.dataProviders = dataProviders;
this.logger = logger;
}
void gatherStringDataOfPlayer(UUID playerUUID, String playerName, Conditions conditions) {
@ -101,10 +98,7 @@ class StringProviderValueGatherer {
}
MethodWrapper<String> method = stringProvider.getMethod();
String result = getMethodResult(
methodCaller.apply(method),
throwable -> pluginName + " has invalid implementation, method " + method.getMethodName() + " threw exception: " + throwable.toString()
);
String result = getMethodResult(methodCaller.apply(method), method);
if (result == null) {
return;
}
@ -116,12 +110,11 @@ class StringProviderValueGatherer {
database.executeTransaction(storeTransactionCreator.apply(method, result));
}
private <T> T getMethodResult(Callable<T> callable, Function<Throwable, String> errorMsg) {
private <T> T getMethodResult(Callable<T> callable, MethodWrapper<String> method) {
try {
return callable.call();
} catch (Exception | NoSuchFieldError | NoSuchMethodError e) {
logger.warn(errorMsg.apply(e));
return null;
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
throw new DataExtensionMethodCallException(e, pluginName, method);
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.extension.implementation.providers.gathering;
import com.djrapitops.plan.api.exceptions.DataExtensionMethodCallException;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.transactions.Transaction;
import com.djrapitops.plan.extension.DataExtension;
@ -30,7 +31,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.provide
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StorePlayerTableResultTransaction;
import com.djrapitops.plan.extension.implementation.storage.transactions.results.StoreServerTableResultTransaction;
import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plugin.logging.console.PluginLogger;
import java.util.Optional;
import java.util.UUID;
@ -51,20 +51,17 @@ class TableProviderValueGatherer {
private final Database database;
private final DataProviders dataProviders;
private final PluginLogger logger;
TableProviderValueGatherer(
String pluginName, DataExtension extension,
UUID serverUUID, Database database,
DataProviders dataProviders,
PluginLogger logger
DataProviders dataProviders
) {
this.pluginName = pluginName;
this.extension = extension;
this.serverUUID = serverUUID;
this.database = database;
this.dataProviders = dataProviders;
this.logger = logger;
}
void gatherTableDataOfPlayer(UUID playerUUID, String playerName, Conditions conditions) {
@ -102,10 +99,7 @@ class TableProviderValueGatherer {
}
MethodWrapper<Table> method = tableProvider.getMethod();
Table result = getMethodResult(
methodCaller.apply(method),
throwable -> pluginName + " has invalid implementation, method " + method.getMethodName() + " threw exception: " + throwable.toString()
);
Table result = getMethodResult(methodCaller.apply(method), method);
if (result == null) {
return;
}
@ -119,12 +113,11 @@ class TableProviderValueGatherer {
database.executeTransaction(storeTransactionCreator.apply(method, result));
}
private <T> T getMethodResult(Callable<T> callable, Function<Throwable, String> errorMsg) {
private <T> T getMethodResult(Callable<T> callable, MethodWrapper<T> method) {
try {
return callable.call();
} catch (Exception | NoSuchFieldError | NoSuchMethodError e) {
logger.warn(errorMsg.apply(e));
return null;
} catch (Exception | NoClassDefFoundError | NoSuchFieldError | NoSuchMethodError e) {
throw new DataExtensionMethodCallException(e, pluginName, method);
}
}

View File

@ -1,3 +1,19 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.utilities.html.tables;
import com.djrapitops.plan.api.PlanAPI;