Skip to content

Commit 9b1342b

Browse files
authored
[#485] Fix concurrency crashes in chunk obfuscation and proximity checks (#486)
* fix: prevent concurrent modification in accessor managers * fix: prevent concurrent modification in wrapped chunk packet data * fix: catch proximity system errors correctly * fix: schedule proximity checks for obfuscated chunks * ci: fix Minecraft 1.21.11 cache configuration * chore: add thread and executor details to diagnostic dumps
1 parent a66c4e3 commit 9b1342b

12 files changed

Lines changed: 188 additions & 66 deletions

File tree

.github/workflows/buildtools.sh

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,27 @@ build () {
1919
checkVersion () {
2020
echo Checking version $1
2121

22-
if [ ! -d ~/.m2/repository/org/spigotmc/spigot/$1-R0.1-SNAPSHOT ]; then
23-
build $1 $2
22+
if [ ! -d ~/.m2/repository/org/spigotmc/spigot/$1-$2-SNAPSHOT ]; then
23+
build $1 $3
2424
fi
2525
}
2626

27-
checkVersion "1.16.5" "8"
28-
checkVersion "1.17.1" "17"
29-
checkVersion "1.18.1" "17"
30-
checkVersion "1.18.2" "17"
31-
checkVersion "1.19.2" "17"
32-
checkVersion "1.19.3" "17"
33-
checkVersion "1.19.4" "17"
34-
checkVersion "1.20.1" "17"
35-
checkVersion "1.20.2" "17"
36-
checkVersion "1.20.4" "17"
37-
checkVersion "1.20.6" "21"
38-
checkVersion "1.21.1" "21"
39-
checkVersion "1.21.3" "21"
40-
checkVersion "1.21.4" "21"
41-
checkVersion "1.21.5" "21"
42-
checkVersion "1.21.8" "21"
43-
checkVersion "1.21.10" "21"
44-
checkVersion "1.21.11" "21"
45-
checkVersion "26.1" "25"
27+
checkVersion "1.16.5" "R0.1" "8"
28+
checkVersion "1.17.1" "R0.1" "17"
29+
checkVersion "1.18.1" "R0.1" "17"
30+
checkVersion "1.18.2" "R0.1" "17"
31+
checkVersion "1.19.2" "R0.1" "17"
32+
checkVersion "1.19.3" "R0.1" "17"
33+
checkVersion "1.19.4" "R0.1" "17"
34+
checkVersion "1.20.1" "R0.1" "17"
35+
checkVersion "1.20.2" "R0.1" "17"
36+
checkVersion "1.20.4" "R0.1" "17"
37+
checkVersion "1.20.6" "R0.1" "21"
38+
checkVersion "1.21.1" "R0.1" "21"
39+
checkVersion "1.21.3" "R0.1" "21"
40+
checkVersion "1.21.4" "R0.1" "21"
41+
checkVersion "1.21.5" "R0.1" "21"
42+
checkVersion "1.21.8" "R0.1" "21"
43+
checkVersion "1.21.10" "R0.1" "21"
44+
checkVersion "1.21.11" "R0.2" "21"
45+
checkVersion "26.1.2" "R0.1" "25"

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/OrebfuscatorDumpFile.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ private void initialize() {
5959
set("versions.orebfuscator", orebfuscator.orebfuscatorVersion().toString());
6060

6161
orebfuscator.systemMonitor().dump(createSection("system"));
62+
orebfuscator.executor().dump(createSection("executor"));
6263

6364
var statistics = createSection("statistics");
6465
orebfuscator.statisticsRegistry().entries().stream()

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/obfuscation/DeobfuscationWorker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void deobfuscate(WorldAccessor world, @Nullable List<BlockPos> blocks) {
7979
return;
8080
}
8181

82-
var timer = statistics.debofuscation.start();
82+
var timer = statistics.deobfuscation.start();
8383
try {
8484
this.deobfuscateNow(world, blocks);
8585
} finally {

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/player/OrebfuscatorPlayer.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Map;
55
import java.util.Objects;
66
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.atomic.AtomicBoolean;
78
import java.util.concurrent.atomic.AtomicReference;
89
import org.jspecify.annotations.NullMarked;
910
import org.jspecify.annotations.Nullable;
@@ -22,6 +23,8 @@ public class OrebfuscatorPlayer {
2223

2324
private final AtomicReference<@Nullable WorldAccessor> world = new AtomicReference<>();
2425
private final Map<Long, OrebfuscatorPlayerChunk> chunks = new ConcurrentHashMap<>();
26+
27+
private final AtomicBoolean pendingUpdate = new AtomicBoolean(false);
2528

2629
private volatile long latestUpdateTimestamp = System.currentTimeMillis();
2730
private volatile EntityPose location = EntityPose.ZERO;
@@ -45,6 +48,13 @@ public boolean needsProximityUpdate(boolean rotation) {
4548
}
4649

4750
long timestamp = System.currentTimeMillis();
51+
if (this.pendingUpdate.compareAndSet(true, false)) {
52+
this.location = player.pose();
53+
this.latestUpdateTimestamp = timestamp;
54+
55+
return true;
56+
}
57+
4858
if (this.config.hasProximityPlayerCheckInterval()
4959
&& timestamp - this.latestUpdateTimestamp > this.config.proximityPlayerCheckInterval()) {
5060

@@ -106,6 +116,13 @@ public void addChunk(WorldAccessor world, int chunkX, int chunkZ, List<Proximity
106116
if (Objects.equals(this.world.getAcquire(), world)) {
107117
long key = ChunkAccessor.chunkCoordsToLong(chunkX, chunkZ);
108118
this.chunks.put(key, new OrebfuscatorPlayerChunk(chunkX, chunkZ, blocks));
119+
120+
int dChunkX = chunkX - (this.location.blockX() >> 4);
121+
int dChunkZ = chunkZ - (this.location.blockZ() >> 4);
122+
123+
if (dChunkX >= -1 && dChunkX <= 1 && dChunkZ >= -1 && dChunkZ <= 1) {
124+
this.pendingUpdate.set(true);
125+
}
109126
}
110127
}
111128

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/proximity/ProximitySystem.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,51 @@ public void start() {
4444
@Override
4545
public void run() {
4646
long processStart = System.nanoTime();
47-
process().whenComplete((v, throwable) -> {
48-
if (throwable != null) {
49-
OfcLogger.error("An error occurred while running proximity worker", throwable);
50-
}
51-
52-
if (this.executor.isShutdown()) {
53-
return;
54-
}
55-
56-
long processTime = System.nanoTime() - processStart;
57-
this.statistics.proximityProcess.add(processTime);
58-
59-
// check if we have enough time to sleep
60-
long waitTime = Math.max(0, this.checkInterval - processTime);
61-
long waitMillis = TimeUnit.NANOSECONDS.toMillis(waitTime);
62-
63-
if (waitMillis > 0) {
64-
// measure wait time
65-
this.statistics.proximityWait.add(TimeUnit.MILLISECONDS.toNanos(waitMillis));
66-
this.executor.schedule(this, waitMillis, TimeUnit.MILLISECONDS);
67-
} else {
68-
this.executor.execute(this);
47+
invokeProcess().whenComplete((v, throwable) -> {
48+
try {
49+
if (throwable != null) {
50+
OfcLogger.error("An error occurred while running proximity worker", throwable);
51+
}
52+
53+
if (this.executor.isShutdown()) {
54+
return;
55+
}
56+
57+
long processTime = System.nanoTime() - processStart;
58+
this.statistics.proximityProcess.add(processTime);
59+
60+
// check if we have enough time to sleep
61+
long waitTime = Math.max(0, this.checkInterval - processTime);
62+
long waitMillis = TimeUnit.NANOSECONDS.toMillis(waitTime);
63+
64+
if (waitMillis > 0) {
65+
// measure wait time
66+
this.statistics.proximityWait.add(TimeUnit.MILLISECONDS.toNanos(waitMillis));
67+
this.executor.schedule(this, waitMillis, TimeUnit.MILLISECONDS);
68+
} else {
69+
// Schedule instead of executing directly to break reentrant execution chains of
70+
// OrebfuscatorExecutor and avoid potential StackOverflowError.
71+
this.statistics.proximityWait.add(0);
72+
this.executor.schedule(this, 0L, TimeUnit.NANOSECONDS);
73+
}
74+
} catch (Exception e) {
75+
OfcLogger.error("An error occurred while scheduling proximity worker", e);
76+
77+
// backoff for 1sec on reschedule failure
78+
this.statistics.proximityWait.add(TimeUnit.SECONDS.toNanos(1));
79+
this.executor.schedule(this, 1L, TimeUnit.SECONDS);
6980
}
7081
});
7182
}
7283

84+
private CompletableFuture<Void> invokeProcess() {
85+
try {
86+
return process();
87+
} catch (Exception e) {
88+
return CompletableFuture.failedFuture(e);
89+
}
90+
}
91+
7392
private CompletableFuture<Void> process() {
7493
var players = this.orebfuscator.players();
7594
if (players.isEmpty()) {

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/statistics/ObfuscationStatistics.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package dev.imprex.orebfuscator.statistics;
22

3-
import java.util.Map;
43
import java.util.StringJoiner;
54
import java.util.function.BiConsumer;
6-
import java.util.function.Consumer;
75
import dev.imprex.orebfuscator.util.RollingAverage;
86
import dev.imprex.orebfuscator.util.RollingTimer;
97

108
public class ObfuscationStatistics implements StatisticsSource {
119

12-
public final RollingTimer debofuscation = new RollingTimer(4096);
10+
public final RollingTimer deobfuscation = new RollingTimer(4096);
1311

1412
public final RollingTimer executorWaitTime = new RollingTimer(4096);
1513
public final RollingTimer executorUtilization = new RollingTimer(4096);
@@ -24,10 +22,10 @@ public class ObfuscationStatistics implements StatisticsSource {
2422

2523
@Override
2624
public void add(StringJoiner joiner) {
27-
long debofuscation = (long) this.debofuscation.average();
25+
long deobfuscation = (long) this.deobfuscation.average();
2826

29-
joiner.add(String.format(" - debofuscation: %s",
30-
time(debofuscation)));
27+
joiner.add(String.format(" - deobfuscation: %s",
28+
time(deobfuscation)));
3129

3230
long executorWaitTime = (long) this.executorWaitTime.average();
3331
double executorUtilization = this.executorUtilization.average();
@@ -66,13 +64,13 @@ public void add(StringJoiner joiner) {
6664

6765
@Override
6866
public void debug(BiConsumer<String, String> consumer) {
69-
consumer.accept("debofuscation", this.debofuscation.debugLong(this::time));
67+
consumer.accept("deobfuscation", this.deobfuscation.debugLong(this::time));
7068

7169
consumer.accept("executorWaitTime", this.executorWaitTime.debugLong(this::time));
7270
consumer.accept("executorUtilization", this.executorUtilization.debugDouble(this::percent));
7371

7472
consumer.accept("proximityWait", this.proximityWait.debugLong(this::time));
75-
consumer.accept("proximityProcess", this.proximityProcess.debugDouble(this::percent));
73+
consumer.accept("proximityProcess", this.proximityProcess.debugLong(this::time));
7674

7775
consumer.accept("missingNeighboringChunks", this.missingNeighboringChunks.debugDouble(this::faction));
7876

orebfuscator-core/src/main/java/dev/imprex/orebfuscator/util/concurrent/OrebfuscatorExecutor.java

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
package dev.imprex.orebfuscator.util.concurrent;
22

3+
import dev.imprex.orebfuscator.config.yaml.ConfigurationSection;
4+
import java.util.Arrays;
5+
import java.util.List;
36
import java.util.Objects;
7+
import java.util.concurrent.BlockingQueue;
48
import java.util.concurrent.Executor;
59
import java.util.concurrent.ExecutorService;
610
import java.util.concurrent.Executors;
11+
import java.util.concurrent.LinkedBlockingQueue;
712
import java.util.concurrent.ScheduledExecutorService;
813
import java.util.concurrent.ScheduledFuture;
14+
import java.util.concurrent.ScheduledThreadPoolExecutor;
15+
import java.util.concurrent.ThreadPoolExecutor;
916
import java.util.concurrent.TimeUnit;
1017
import java.util.concurrent.atomic.LongAdder;
18+
import java.util.stream.Collectors;
1119
import org.jspecify.annotations.NullMarked;
1220
import dev.imprex.orebfuscator.interop.OrebfuscatorCore;
1321
import dev.imprex.orebfuscator.statistics.OrebfuscatorStatistics;
1422

1523
@NullMarked
1624
public class OrebfuscatorExecutor implements Executor {
1725

18-
private final ScheduledExecutorService scheduledExecutorService = Executors
19-
.newSingleThreadScheduledExecutor(r -> new Thread(OrebfuscatorCore.THREAD_GROUP, r, "orebfuscator-scheduler"));
26+
private final ScheduledThreadPoolExecutor scheduledExecutorService = new ScheduledThreadPoolExecutor(
27+
1, r -> new Thread(OrebfuscatorCore.THREAD_GROUP, r, "orebfuscator-scheduler"));
2028

2129
private final int poolSize;
22-
private final ExecutorService executorService;
30+
private final ThreadPoolExecutor executorService;
2331

2432
private final LongAdder run = new LongAdder();
2533
private long updateTime = System.nanoTime();
@@ -30,7 +38,11 @@ public OrebfuscatorExecutor(OrebfuscatorCore orebfuscator) {
3038
this.statistics = orebfuscator.statistics();
3139

3240
this.poolSize = orebfuscator.config().advanced().threads();
33-
this.executorService = Executors.newFixedThreadPool(this.poolSize, OrebfuscatorThread::new);
41+
this.executorService = new ThreadPoolExecutor(
42+
this.poolSize, this.poolSize,
43+
0L, TimeUnit.MILLISECONDS,
44+
new LinkedBlockingQueue<>(),
45+
OrebfuscatorThread::new);
3446

3547
this.scheduledExecutorService.scheduleAtFixedRate(this::updateStatistics, 1L, 1L, TimeUnit.SECONDS);
3648
}
@@ -65,6 +77,79 @@ public void shutdown() {
6577
this.executorService.shutdownNow();
6678
}
6779

80+
public void dump(ConfigurationSection section) {
81+
dumpExecutor(section.createSection("scheduler"), this.scheduledExecutorService);
82+
dumpExecutor(section.createSection("core"), this.executorService);
83+
84+
ConfigurationSection threads = section.createSection("threads");
85+
86+
for (var entry : Thread.getAllStackTraces().entrySet()) {
87+
Thread thread = entry.getKey();
88+
89+
if (!(thread instanceof OrebfuscatorThread)) {
90+
continue;
91+
}
92+
93+
StackTraceElement[] stackTrace = entry.getValue();
94+
95+
ConfigurationSection threadSection =
96+
threads.createSection("thread-" + thread.getId());
97+
98+
threadSection.set("name", thread.getName());
99+
threadSection.set("id", thread.getId());
100+
threadSection.set("state", thread.getState().name());
101+
threadSection.set("alive", thread.isAlive());
102+
threadSection.set("daemon", thread.isDaemon());
103+
threadSection.set("priority", thread.getPriority());
104+
threadSection.set("stack", formatStackTrace(stackTrace));
105+
}
106+
}
107+
108+
private static void dumpExecutor(ConfigurationSection section, ThreadPoolExecutor executor) {
109+
section.set("shutdown", executor.isShutdown());
110+
section.set("terminating", executor.isTerminating());
111+
section.set("terminated", executor.isTerminated());
112+
113+
section.set("pool.size", executor.getPoolSize());
114+
section.set("pool.coreSize", executor.getCorePoolSize());
115+
section.set("pool.maxSize", executor.getMaximumPoolSize());
116+
section.set("pool.largestSize", executor.getLargestPoolSize());
117+
section.set("pool.active", executor.getActiveCount());
118+
119+
long taskCount = executor.getTaskCount();
120+
long completedTaskCount = executor.getCompletedTaskCount();
121+
122+
section.set("tasks.total", taskCount);
123+
section.set("tasks.completed", completedTaskCount);
124+
section.set("tasks.pending", Math.max(0, taskCount - completedTaskCount));
125+
126+
BlockingQueue<Runnable> queue = executor.getQueue();
127+
section.set("queue.type", queue.getClass().getName());
128+
section.set("queue.size", queue.size());
129+
section.set("queue.remainingCapacity", queue.remainingCapacity());
130+
131+
section.set("keepAliveMillis", executor.getKeepAliveTime(TimeUnit.MILLISECONDS));
132+
section.set("allowsCoreThreadTimeOut", executor.allowsCoreThreadTimeOut());
133+
134+
section.set("threadFactory", executor.getThreadFactory().getClass().getName());
135+
section.set("rejectedExecutionHandler", executor.getRejectedExecutionHandler().getClass().getName());
136+
137+
if (executor instanceof ScheduledThreadPoolExecutor scheduled) {
138+
section.set("scheduled.continueExistingPeriodicTasksAfterShutdown",
139+
scheduled.getContinueExistingPeriodicTasksAfterShutdownPolicy());
140+
section.set("scheduled.executeExistingDelayedTasksAfterShutdown",
141+
scheduled.getExecuteExistingDelayedTasksAfterShutdownPolicy());
142+
section.set("scheduled.removeOnCancel",
143+
scheduled.getRemoveOnCancelPolicy());
144+
}
145+
}
146+
147+
private static List<String> formatStackTrace(StackTraceElement[] stackTrace) {
148+
return Arrays.stream(stackTrace)
149+
.map(element -> "at " + element)
150+
.collect(Collectors.toList());
151+
}
152+
68153
private void updateStatistics() {
69154
long time = System.nanoTime();
70155
try {

orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/iterop/BukkitChunkPacketAccessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import net.imprex.orebfuscator.util.MinecraftVersion;
1515
import net.imprex.orebfuscator.util.WrappedClientboundLevelChunkPacketData;
1616
import org.jspecify.annotations.NullMarked;
17+
import org.jspecify.annotations.Nullable;
1718

1819
@NullMarked
1920
public class BukkitChunkPacketAccessor implements ChunkPacketAccessor {
@@ -29,7 +30,7 @@ public class BukkitChunkPacketAccessor implements ChunkPacketAccessor {
2930
private final byte[] data;
3031

3132
private final PacketContainer packet;
32-
private final WrappedClientboundLevelChunkPacketData packetData;
33+
private final @Nullable WrappedClientboundLevelChunkPacketData packetData;
3334

3435
public BukkitChunkPacketAccessor(PacketContainer packet, BukkitWorldAccessor worldAccessor) {
3536
this.packet = packet;

orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/iterop/BukkitPlayerAccessor.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.util.Objects;
1313
import java.util.WeakHashMap;
1414
import java.util.concurrent.CompletableFuture;
15-
import java.util.concurrent.atomic.AtomicBoolean;
1615
import java.util.concurrent.atomic.AtomicReference;
1716
import net.imprex.orebfuscator.Orebfuscator;
1817
import net.imprex.orebfuscator.OrebfuscatorCompatibility;

0 commit comments

Comments
 (0)