[BLEEDING][BREAKING] New implementation for exemption. (+)

* Store the per check type flags within PlayerData/PlayerCheckTypeTree.
Access methods within PlayerData.
* (More) thread-safe access, with the twist that alterations are mostly
done within the thread-context (primary thread vs. asynchronous).
(+) Mimic legacy behavior, by non-nested entries. However untouched
nested entries are possible, creating ExemptionContext instances with
other negative ids.
(-) No ExemptionRegistry is implemented. This just aims at replacing the
internals, without altering the (legacy) behavior.
This commit is contained in:
asofold 2018-02-08 12:38:40 +01:00
parent eb41f4397a
commit cec4a4d129
7 changed files with 627 additions and 55 deletions

View File

@ -0,0 +1,65 @@
package fr.neatmonster.nocheatplus.hooks;
/**
* Both hashCode and equals only compare the ids, so equals is not fit for
* comparing side conditions - AWAIT a method like
* isEquivalentTo(ExemptionContext) for that purpose, rather.
* <hr>
* Note that some ids like 0 and -1 are reserved. The registry will only deal
* positive ids.
*
* @author asofold
*
*/
public class ExemptionContext {
/** Id is -1. */
public static final ExemptionContext LEGACY_NON_NESTED = new ExemptionContext(-1);
/** Id is 0. */
public static final ExemptionContext ANONYMOUS_NESTED = new ExemptionContext(0);
///////////////
// Instance
///////////////
/*
*
* TODO: How to use (one context = one thing, or one context contains multiple ids.
* -> so contexts contain contexts (...).
*/
private Integer id;
public ExemptionContext(final Integer id) {
if (id == null) {
throw new NullPointerException("The id must not be null.");
}
this.id = id;
}
public Integer getId() {
return id;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
else if (obj instanceof ExemptionContext) {
return id.equals(((ExemptionContext) obj).getId());
}
else if (obj instanceof Integer) {
return id.equals((Integer) obj);
}
else {
return false;
}
}
}

View File

@ -0,0 +1,12 @@
package fr.neatmonster.nocheatplus.hooks;
/**
* Register contexts, get ids >= 0 for new contexts.
* @author asofold
*
*/
public class ExemptionRegistry {
// TODO: ...
}

View File

@ -14,11 +14,6 @@
*/
package fr.neatmonster.nocheatplus.hooks;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
@ -26,10 +21,20 @@ import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.utilities.CheckTypeUtil;
import fr.neatmonster.nocheatplus.players.DataManager;
import fr.neatmonster.nocheatplus.players.PlayerData;
/**
* API for exempting players of checks, checked before calculations are done.
* <hr>
* Refactoring stage: Route to DataManager/PlayerData, which will somehow
* attempt to mimic legacy behavior. Do note that checking for meta data and
* other specialties are not handled within DataManager/PlayerData yet.
* <hr>
* Except clear and accessing settings, it will be "safe" to access methods
* asynchronously to the server primary thread, however isExempted always
* reflects a non-synchronized state of all thread contexts, while exempt and
* unexempt will be local to the thread-context (primary thread, asynchronous).
*
* @author asofold
*/
@ -37,16 +42,6 @@ public class NCPExemptionManager {
private static ExemptionSettings settings = new ExemptionSettings();
/**
* A map associating a check type with the unique ids of its exempted
* players.
*/
private static final Map<CheckType, Set<UUID>> exempted = new HashMap<CheckType, Set<UUID>>();
static {
clear();
}
/**
* Get the current settings.
*
@ -78,15 +73,7 @@ public class NCPExemptionManager {
* Remove all exemptions.
*/
public static final void clear() {
// Use put with a new map to keep entries to stay thread safe.
for (final CheckType checkType : CheckType.values()) {
if (CheckTypeUtil.needsSynchronization(checkType)) {
exempted.put(checkType, Collections.synchronizedSet(new HashSet<UUID>()));
}
else {
exempted.put(checkType, new HashSet<UUID>());
}
}
DataManager.clearAllExemptions();
}
/**
@ -109,9 +96,11 @@ public class NCPExemptionManager {
* The check type.
*/
public static final void exemptPermanently(final UUID id, final CheckType checkType) {
for (final CheckType refType : CheckTypeUtil.getWithDescendants(checkType)) {
exempted.get(refType).add(id);
final PlayerData data = DataManager.getPlayerData(id);
if (data != null) {
data.exempt(checkType);
}
// TODO: else throw ?
}
/**
@ -152,7 +141,8 @@ public class NCPExemptionManager {
* @return If the entity is exempted from checks right now.
*/
public static final boolean isExempted(final UUID id, final CheckType checkType) {
return exempted.get(checkType).contains(id);
final PlayerData data = DataManager.getPlayerData(id);
return data != null && data.isExempted(checkType);
}
/**
@ -211,8 +201,9 @@ public class NCPExemptionManager {
* The check type.
*/
public static final void unexempt(final UUID id, final CheckType checkType) {
for (final CheckType refType : CheckTypeUtil.getWithDescendants(checkType)) {
exempted.get(refType).remove(id);
final PlayerData data = DataManager.getPlayerData(id);
if (data != null) {
data.unexempt(checkType);
}
}

View File

@ -1036,4 +1036,14 @@ public class DataManager implements INeedConfig, ComponentRegistry<IRemoveData>,
instance.frequentPlayerTasks.addAsynchronous(playerId);
}
/**
*
*/
public static void clearAllExemptions() {
final Iterator<Entry<UUID, PlayerData>> it = instance.playerData.iterator();
while (it.hasNext()) {
it.next().getValue().clearAllExemptions();
}
}
}

View File

@ -0,0 +1,370 @@
package fr.neatmonster.nocheatplus.players;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import org.bukkit.Bukkit;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.components.data.checktype.CheckTypeTree;
import fr.neatmonster.nocheatplus.components.data.checktype.CheckTypeTree.CheckTypeTreeNode;
import fr.neatmonster.nocheatplus.hooks.ExemptionContext;
import fr.neatmonster.nocheatplus.players.PlayerCheckTypeTree.PlayerCheckTypeTreeNode;
/**
* <hr>
* Exemption and unexemption are handled within two contexts: primary thread and
* asynchronous. Exemption testing may account for both states (without
* synchronization).
*
* @author asofold
*
*/
public class PlayerCheckTypeTree extends CheckTypeTree<PlayerCheckTypeTreeNode>{
/**
* <hr>
* <ul>
* <li>Nodes are not meant to be passed to the outside (!).</li>
* <li>Locking for 'asynchronous' access has to be done externally.</li>
* </ul>
*
* @author asofold
*
*/
static class PlayerCheckTypeTreeNode extends CheckTypeTreeNode<PlayerCheckTypeTreeNode> {
/**
* Explicitly exempted by API call (cumulative flag), excludes checking for
* meta data and the like.
*/
private boolean exemptedPrimaryThread = false;
/**
* Explicitly exempted by API call (cumulative flag), excludes checking for
* meta data and the like.
*/
private boolean exemptedAsynchronous = false;
/**
* Exemption contexts that apply here. Lazily allocate.
*/
/*
* TODO: Consider a ParallelList (access independently without need for
* mergePrimaryThread, or extend DualList by appropriate methods).
*/
private List<ExemptionContext> exemptionsPrimaryThread = null;
private List<ExemptionContext> exemptionsAsynchronous = null;
PlayerCheckTypeTreeNode(final CheckType checkType,
final PlayerCheckTypeTreeNode parent,
final CheckTypeTreeNodeFactory<PlayerCheckTypeTreeNode> factory) {
super(checkType, parent, factory);
}
/**
* Check for exemption by explicit API call. Excludes checking for meta
* data or other API.
* <hr>
* No locking involved, checking flags for primary thread and
* asynchronous access.
*
* @return
*/
boolean isExempted() {
// Typically this should boil down to thread-local use, as it is CheckType-bound.
return exemptedPrimaryThread || exemptedAsynchronous;
}
/**
* Must call under lock.
*
* @param context
*/
void exemptAsynchronous(final ExemptionContext context) {
exemptedAsynchronous = true;
if (exemptionsAsynchronous == null) {
exemptionsAsynchronous = new LinkedList<ExemptionContext>();
}
exemptionsAsynchronous.add(0, context);
}
void exemptPrimaryThread(final ExemptionContext context) {
exemptedPrimaryThread = true;
if (exemptionsPrimaryThread == null) {
exemptionsPrimaryThread = new LinkedList<ExemptionContext>();
}
exemptionsPrimaryThread.add(0, context);
}
/**
* Must call under lock.
*
* @param context
*/
void unexemptAsynchronous(final ExemptionContext context) {
if (exemptionsAsynchronous != null) {
exemptionsAsynchronous.remove(context);
if (exemptionsAsynchronous.isEmpty()) {
// TODO: Have a counter to delay resetting?
exemptedAsynchronous = false;
exemptionsAsynchronous = null;
}
}
}
void unexemptPrimaryThread(final ExemptionContext context) {
if (exemptionsPrimaryThread != null) {
exemptionsPrimaryThread.remove(context);
if (exemptionsPrimaryThread.isEmpty()) {
// TODO: Have a counter to delay resetting?
exemptedPrimaryThread = false;
exemptionsPrimaryThread = null;
}
}
}
/**
* Must call under lock.
*
* @param context
*/
void unexemptAllAsynchronous(final ExemptionContext context) {
if (exemptionsAsynchronous != null) {
exemptionsAsynchronous.removeAll(Collections.singleton(context));
if (exemptionsAsynchronous.isEmpty()) {
// TODO: Have a counter to delay resetting?
exemptedAsynchronous = false;
exemptionsAsynchronous = null;
}
}
}
void unexemptAllPrimaryThread(final ExemptionContext context) {
if (exemptionsPrimaryThread != null) {
exemptionsPrimaryThread.removeAll(Collections.singleton(context));
if (exemptionsPrimaryThread.isEmpty()) {
// TODO: Have a counter to delay resetting?
exemptedPrimaryThread = false;
exemptionsPrimaryThread = null;
}
}
}
/**
* Must call under lock.
*
* @param context
* @return
*/
boolean isExemptedAsynchronous(ExemptionContext context) {
return exemptionsAsynchronous != null && exemptionsAsynchronous.contains(context);
}
boolean isExemptedPrimaryThread(final ExemptionContext context) {
return exemptionsPrimaryThread != null && exemptionsPrimaryThread.contains(context);
}
/**
* Primary thread only, must call under lock.
*/
public void clearAllExemptions() {
exemptedAsynchronous = false;
exemptedPrimaryThread = false;
exemptionsAsynchronous = null;
exemptionsPrimaryThread = null;
}
}
////////////////
// Instance
////////////////
private final Lock lock;
public PlayerCheckTypeTree(final Lock lock) {
super();
this.lock = lock;
}
@Override
protected PlayerCheckTypeTreeNode newNode(CheckType checkType,
PlayerCheckTypeTreeNode parent,
CheckTypeTreeNodeFactory<PlayerCheckTypeTreeNode> factory) {
return new PlayerCheckTypeTreeNode(checkType, parent, factory);
}
/**
* <hr>
* Thread-safe read, not synchronized.
*
* @param checkType
* @return
*/
public boolean isExempted(final CheckType checkType) {
return getNode(checkType).isExempted(); // Fast read.
}
public void exempt(final CheckType checkType, final ExemptionContext context) {
final PlayerCheckTypeTreeNode node = getNode(checkType);
if (node == null) {
throw new IllegalArgumentException("Invalid check type.");
}
if (Bukkit.isPrimaryThread()) {
exemptPrimaryThread(node, context);
}
else {
exemptAsynchronous(node, context);
}
}
private void exemptPrimaryThread(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.exemptPrimaryThread(context);
return true;
}
});
}
private void exemptAsynchronous(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
lock.lock();
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.exemptAsynchronous(context);
return true;
}
});
lock.unlock();
}
public void unexempt(final CheckType checkType, final ExemptionContext context) {
final PlayerCheckTypeTreeNode node = getNode(checkType);
if (node == null) {
throw new IllegalArgumentException("Invalid check type.");
}
if (Bukkit.isPrimaryThread()) {
unexemptPrimaryThread(node, context);
}
else {
unexemptAsynchronous(node, context);
}
}
private void unexemptPrimaryThread(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.unexemptPrimaryThread(context);
return true;
}
});
}
private void unexemptAsynchronous(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
lock.lock();
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.unexemptAsynchronous(context);
return true;
}
});
lock.unlock();
}
public void unexemptAll(final CheckType checkType, final ExemptionContext context) {
final PlayerCheckTypeTreeNode node = getNode(checkType);
if (node == null) {
throw new IllegalArgumentException("Invalid check type.");
}
if (Bukkit.isPrimaryThread()) {
unexemptAllPrimaryThread(node, context);
}
else {
unexemptAllAsynchronous(node, context);
}
}
private void unexemptAllPrimaryThread(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.unexemptAllPrimaryThread(context);
return true;
}
});
}
private void unexemptAllAsynchronous(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
lock.lock();
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.unexemptAllAsynchronous(context);
return true;
}
});
lock.unlock();
}
public boolean isExempted(final CheckType checkType, final ExemptionContext context) {
final PlayerCheckTypeTreeNode node = getNode(checkType);
if (node == null) {
throw new IllegalArgumentException("Invalid check type.");
}
if (Bukkit.isPrimaryThread()) {
return isExemptedPrimaryThread(node, context);
}
else {
return isExemptedAsynchronous(node, context);
}
}
private boolean isExemptedPrimaryThread(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
return node.isExemptedPrimaryThread(context);
}
private boolean isExemptedAsynchronous(final PlayerCheckTypeTreeNode node, final ExemptionContext context) {
final boolean res;
lock.lock();
res = node.isExemptedPrimaryThread(context);
lock.unlock();
return res;
}
/**
* Call from the primary thread only.
*
*/
public void clearAllExemptions() {
clearAllExemptions(CheckType.ALL);
}
/**
* Call from the primary thread only.
*
* @param checkType
*/
public void clearAllExemptions(final CheckType checkType) {
final PlayerCheckTypeTreeNode node = getNode(checkType);
if (node == null) {
throw new IllegalArgumentException("Invalid check type.");
}
lock.lock();
visitWithDescendants(node, new Visitor<PlayerCheckTypeTreeNode>() {
@Override
public boolean visit(final PlayerCheckTypeTreeNode node) {
node.clearAllExemptions();
return true;
}
});
lock.unlock();
}
}

View File

@ -21,15 +21,19 @@ import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
import fr.neatmonster.nocheatplus.compat.AlmostBoolean;
import fr.neatmonster.nocheatplus.components.data.ICanHandleTimeRunningBackwards;
import fr.neatmonster.nocheatplus.components.data.IData;
import fr.neatmonster.nocheatplus.hooks.ExemptionContext;
import fr.neatmonster.nocheatplus.permissions.PermissionInfo;
import fr.neatmonster.nocheatplus.permissions.PermissionNode;
import fr.neatmonster.nocheatplus.permissions.PermissionPolicy.FetchingPolicy;
@ -107,17 +111,21 @@ public class PlayerData implements IData, ICanHandleTimeRunningBackwards {
// Instance //
//////////////
// TODO: Use the same lock for permissions stuff ?
/** Per player lock. */
/*
* TODO: Impact of using this everywhere is uncertain. For exemptions and
* permissions it'll be ok, because nodes get created once for most, but for
* permission updates (merge primary thread) and the like, it'll not be as
* certain.
*/
private final Lock lock = new ReentrantLock();
/** Not sure this is the future of extra properties. */
private Set<String> tags = null;
/*
* TODO: Consider updating the UUID for stuff like
* "exempt player/name on next login". This also implies the addition of a
* method to force-postpone data removal, as well as configuration for how
* exactly to apply/timeout, plus new syntax for 'ncp exempt' (flags/side
* conditions like +login/...).
* TODO: Concept for updating names for UUIDs -> + OfflinePlayerData,
* uncertain when/how to access.
*/
/** Unique id of the player. */
final UUID playerId;
@ -138,16 +146,19 @@ public class PlayerData implements IData, ICanHandleTimeRunningBackwards {
private final PermissionRegistry permissionRegistry;
/** Permission cache. */
private final HashMapLOW<Integer, PermissionNode> permissions = new HashMapLOW<Integer, PermissionNode>(35);
private final HashMapLOW<Integer, PermissionNode> permissions = new HashMapLOW<Integer, PermissionNode>(lock, 35);
private boolean requestUpdateInventory = false;
private boolean requestPlayerSetBack = false;
private boolean frequentPlayerTaskShouldBeScheduled = false;
/** Actually queried ones. */
private final DualSet<RegisteredPermission> updatePermissions = new DualSet<RegisteredPermission>();
private final DualSet<RegisteredPermission> updatePermissions = new DualSet<RegisteredPermission>(lock);
/** Possibly needed in future. */
private final DualSet<RegisteredPermission> updatePermissionsLazy = new DualSet<RegisteredPermission>();
private final DualSet<RegisteredPermission> updatePermissionsLazy = new DualSet<RegisteredPermission>(lock);
/** TODO: Soon to add minimized offline data, so these kind of things don't impact as much. */
private final PlayerCheckTypeTree checkTypeTree = new PlayerCheckTypeTree(lock);
/** Unregister the tasks once 0 count is reached. */
private short frequentTaskDelayUnregister = 0;
@ -656,4 +667,118 @@ public class PlayerData implements IData, ICanHandleTimeRunningBackwards {
updatePermissionsLazy.clearPrimaryThread();
}
/**
* Mimic legacy behavior (non-nested) - exempt including descendants
* recursively. Note that contexts other than
* ExemptionContext.LEGACY_NON_NESTED will not be touched.
*
* @param checkType
*/
public void exempt(final CheckType checkType) {
checkTypeTree.exempt(checkType, ExemptionContext.LEGACY_NON_NESTED);
// TODO: Handlers?
}
/**
* Mimic legacy behavior (non-nested) - unexempt including descendants
* recursively. Note that contexts other than
* ExemptionContext.LEGACY_NON_NESTED will not be touched.
* <hr>
* Primary thread and asynchronous access are separated and yield different
* results, it's imperative to always unexempt properly for asyncrhonous
* thread contexts, as isExempted reflects a mixture of both.
*
* @param checkType
*/
public void unexempt(final CheckType checkType) {
checkTypeTree.unexemptAll(checkType, ExemptionContext.LEGACY_NON_NESTED);
// TODO: Handlers?
}
/**
* Exempt with reference to the given context with descendants recursively.
* <br>
* Note that multiple calls to exempt demand multiple calls to
* unexempt(CheckType, ExemptionContext).
* <hr>
* Primary thread and asynchronous access are separated and yield different
* results, it's imperative to always unexempt properly for asyncrhonous
* thread contexts, as isExempted reflects a mixture of both.
*
* @param checkType
* @param context
*/
public void exempt(final CheckType checkType, final ExemptionContext context) {
checkTypeTree.exempt(checkType, context);
}
/**
* Unexempt once, including descendants recursively. <br>
* Note that for multiple calls to exempt with one context, multiple calls
* to unexempt with that context may be necessary to fully unexempt, or call
* unexemptAll for the context.
* <hr>
* ExemptionContext.LEGACY_NON_NESTED is not automatically calling
* unexemptAll as is done with the legacy signature unexempt(CheckType).
* <hr>
* Primary thread and asynchronous access are separated and yield different
* results, it's imperative to always unexempt properly for asyncrhonous
* thread contexts, as isExempted reflects a mixture of both.
*
* @param checkType
* @param context
*/
public void unexempt(final CheckType checkType, final ExemptionContext context) {
checkTypeTree.unexempt(checkType, context);
}
/**
* Remove all (potentially nested) entries context for the given checkType
* and descendants recursively.
* <hr>
* Primary thread and asynchronous access are separated and yield different
* results, it's imperative to always unexempt properly for asyncrhonous
* thread contexts, as isExempted reflects a mixture of both.
*
* @param checkType
* @param context
*/
public void unexemptAll(final CheckType checkType, final ExemptionContext context) {
checkTypeTree.unexemptAll(checkType, context);
}
/**
* Test for exemption.
* <hr>
* Thread-safe read (not synchronized).
*
* @param checkType
* @return
*/
public boolean isExempted(final CheckType checkType) {
return checkTypeTree.isExempted(checkType);
}
/**
* Clear all exemptions, for all thread contexts.
* <hr>
* Call from the primary thread only.
*/
public void clearAllExemptions() {
checkTypeTree.clearAllExemptions();
}
/**
* Clear all exemptions for the given checkType and descendants recursively,
* for all thread contexts.
* <hr>
* Call from the primary thread only.
*
* @param checkType
*/
public void clearAllExemptions(final CheckType checkType) {
checkTypeTree.clearAllExemptions(checkType);
}
}

View File

@ -181,26 +181,25 @@ public class CheckUtils {
* information.).
*/
/*
* TODO: Once thread-safe read has been implemented, check the fastest
* thing first (likely exemption).
*/
final RegisteredPermission permission = checkType.getPermission();
if (permission != null) {
// Check permission policy/cache regardless of the thread context.
if (pData.hasPermission(permission, player)) {
return true;
}
}
// TODO: Refine this error message +- put in place where it needs to be.
if (!isPrimaryThread && !CheckTypeUtil.needsSynchronization(checkType)) {
// Checking for exemption can cause harm now.
/*
* Checking for exemption can't cause harm anymore, however even
* fetching data or configuration might still lead to everything
* exploding.
*/
improperAPIAccess(checkType);
}
// TODO: New exemption implementation (thread-safe read).
// TODO: Maybe a solution: force sync into primary thread a) each time b) once with lazy force set to use copy on write [for the player or global?].
return NCPExemptionManager.isExempted(player, checkType, isPrimaryThread);
// Exemption check.
if (NCPExemptionManager.isExempted(player, checkType, isPrimaryThread)) {
return true;
}
// Check permission policy/cache regardless of the thread context.
final RegisteredPermission permission = checkType.getPermission();
if (permission != null && pData.hasPermission(permission, player)) {
return true;
}
return false;
}
/**