Compare commits

...
Author SHA1 Message Date
Axionize 96a5e00b09 Fix a rare CME in LatencyUtils 2025-02-03 10:19:17 -05:00
@@ -7,6 +7,7 @@ import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.data.Pair;
import com.github.retrooper.packetevents.netty.channel.ChannelHelper;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;
@@ -14,6 +15,10 @@ public class LatencyUtils {
private final LinkedList<Pair<Integer, Runnable>> transactionMap = new LinkedList<>();
private final GrimPlayer player;
// Built from transactionMap and cleared at start of every handleNettySyncTransaction() call
// The actual usage scope of this variable's use is limited to within the synchronized block of handleNettySyncTransaction
private final ArrayList<Runnable> tasksToRun = new ArrayList<>();
public LatencyUtils(GrimPlayer player) {
this.player = player;
}
@@ -41,21 +46,48 @@ public class LatencyUtils {
}
public void handleNettySyncTransaction(int transaction) {
/*
* This code uses a two-pass approach within the synchronized block to prevent CMEs.
* First we collect and remove tasks using the iterator, then execute all collected tasks.
*
* The issue:
* We cannot execute tasks during iteration because if a runnable modifies transactionMap
* or calls addRealTimeTask, it will cause a ConcurrentModificationException.
* While only seen on Folia servers, this is theoretically possible everywhere.
*
* Why this solution:
* Rather than documenting "don't modify transactionMap in runnables" and risking subtle
* bugs from future contributions or Check API usage, we prevent the issue entirely
* at a small performance cost.
*
* Future considerations:
* If this becomes a performance bottleneck, we may revisit using a single-pass approach
* on non-Folia servers. We could also explore concurrent data structures or parallel
* execution, but this would lose the guarantee that transactions are processed in order.
*/
synchronized (this) {
for (ListIterator<Pair<Integer, Runnable>> iterator = transactionMap.listIterator(); iterator.hasNext(); ) {
tasksToRun.clear();
// First pass: collect tasks and mark them for removal
ListIterator<Pair<Integer, Runnable>> iterator = transactionMap.listIterator();
while (iterator.hasNext()) {
Pair<Integer, Runnable> pair = iterator.next();
// We are at most a tick ahead when running tasks based on transactions, meaning this is too far
if (transaction + 1 < pair.first())
return;
break;
// This is at most tick ahead of what we want
if (transaction == pair.first() - 1)
continue;
tasksToRun.add(pair.second());
iterator.remove();
}
for (Runnable runnable : tasksToRun) {
try {
// Run the task
pair.second().run();
runnable.run();
} catch (Exception e) {
LogUtil.error("An error has occurred when running transactions for player: " + player.user.getName(), e);
// Kick the player SO PEOPLE ACTUALLY REPORT PROBLEMS AND KNOW WHEN THEY HAPPEN
@@ -63,8 +95,6 @@ public class LatencyUtils {
player.disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(player, GrimAPI.INSTANCE.getConfigManager().getDisconnectPacketError())));
}
}
// We ran a task, remove it from the list
iterator.remove();
}
}
}