Skip to content

Commit d9ae896

Browse files
authored
Stability and perf improvements (credit: ChannyAnh)
Merge PR #25: stability/perf hardening, core tests, and Spigot injector race fix.
1 parent a99e2a1 commit d9ae896

21 files changed

Lines changed: 1388 additions & 101 deletions

File tree

core/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ dependencies {
2020
implementation("io.grpc", "grpc-protobuf", Versions.gRPCVersion)
2121
implementation("io.grpc", "grpc-stub", Versions.gRPCVersion)
2222
implementation("javax.annotation", "javax.annotation-api", "1.3.2")
23+
24+
// Test deps — pinned to versions still compatible with the Java 8 source target.
25+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.5")
26+
testImplementation("org.mockito:mockito-core:4.11.0")
27+
testImplementation("org.awaitility:awaitility:4.2.2")
28+
testImplementation("io.netty", "netty-transport", Versions.nettyVersion)
29+
testImplementation("io.netty", "netty-codec", Versions.nettyVersion)
30+
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")
31+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
32+
}
33+
34+
tasks.test {
35+
useJUnitPlatform()
2336
}
2437

2538
// present on all platforms

core/src/main/java/com/minekube/connect/addon/packethandler/ChannelInPacketHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
4646
packetHandlers.getPacketHandlers(msg.getClass())) {
4747

4848
Object res = consumer.apply(ctx, msg, toServer);
49-
if (!res.equals(msg)) {
49+
if (res != msg) {
5050
packet = res;
5151
}
5252
}

core/src/main/java/com/minekube/connect/addon/packethandler/ChannelOutPacketHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) {
4747
packetHandlers.getPacketHandlers(msg.getClass())) {
4848

4949
Object res = consumer.apply(ctx, msg, toServer);
50-
if (!res.equals(msg)) {
50+
if (res != msg) {
5151
packet = res;
5252
}
5353
}

core/src/main/java/com/minekube/connect/inject/CommonPlatformInjector.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@
3030
import io.netty.channel.Channel;
3131
import io.netty.channel.ChannelFuture;
3232
import java.net.SocketAddress;
33-
import java.util.HashMap;
34-
import java.util.HashSet;
3533
import java.util.Map;
3634
import java.util.Set;
35+
import java.util.concurrent.ConcurrentHashMap;
3736
import lombok.AccessLevel;
3837
import lombok.Getter;
3938

@@ -65,10 +64,12 @@ public void shutdown() {
6564
}
6665
}
6766

67+
// Both registries are mutated from Netty I/O threads (per-channel) and iterated
68+
// concurrently, so they must be thread-safe.
6869
@Getter(AccessLevel.PROTECTED)
69-
private final Set<Channel> injectedClients = new HashSet<>();
70+
private final Set<Channel> injectedClients = ConcurrentHashMap.newKeySet();
7071

71-
private final Map<Class<?>, InjectorAddon> addons = new HashMap<>();
72+
private final Map<Class<?>, InjectorAddon> addons = new ConcurrentHashMap<>();
7273

7374
protected boolean addInjectedClient(Channel channel) {
7475
return injectedClients.add(channel);

core/src/main/java/com/minekube/connect/network/netty/LocalSession.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
*/
7070
@RequiredArgsConstructor
7171
public final class LocalSession {
72-
private static final int CONNECTION_TIMEOUT = (int) Duration.ofSeconds(30).toMillis();
72+
private static final int CONNECTION_TIMEOUT = (int) Duration.ofSeconds(10).toMillis();
7373

7474
private static DefaultEventLoopGroup DEFAULT_EVENT_LOOP_GROUP;
7575
private static EventLoopGroup PLATFORM_EVENT_LOOP_GROUP; // Platform-specific event loop group

core/src/main/java/com/minekube/connect/network/netty/TunnelHandler.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,47 @@
3232
import io.netty.buffer.ByteBuf;
3333
import io.netty.buffer.Unpooled;
3434
import io.netty.channel.Channel;
35+
import io.netty.channel.EventLoop;
36+
import java.util.concurrent.RejectedExecutionException;
37+
import java.util.concurrent.atomic.AtomicBoolean;
3538
import lombok.RequiredArgsConstructor;
3639

3740
@RequiredArgsConstructor
3841
class TunnelHandler implements Handler {
3942
private final ConnectLogger logger;
4043
private final Channel downstreamServerConn; // local server connection
4144

45+
// Coalesces flushes across an EventLoop tick: one flush() per batch of
46+
// onReceive calls instead of one per packet. The CAS lives inside the
47+
// write task so the flush is always enqueued after the write that needs
48+
// it — scheduling the CAS outside the EventLoop races, because a later
49+
// write can be enqueued behind an already-scheduled flush.
50+
private final AtomicBoolean flushScheduled = new AtomicBoolean(false);
51+
4252
@Override
4353
public void onReceive(byte[] data) {
44-
// TunnelService -> local session server -> downstream server
45-
ByteBuf buf = Unpooled.wrappedBuffer(data);
46-
downstreamServerConn.writeAndFlush(buf);
54+
// TunnelService -> local session server -> downstream server.
55+
// Allocate the ByteBuf inside the lambda so it isn't leaked if execute()
56+
// rejects (event loop shutting down during proxy stop).
57+
Channel ch = downstreamServerConn;
58+
EventLoop el = ch.eventLoop();
59+
try {
60+
el.execute(() -> {
61+
ch.write(Unpooled.wrappedBuffer(data), ch.voidPromise());
62+
if (flushScheduled.compareAndSet(false, true)) {
63+
try {
64+
el.execute(() -> {
65+
flushScheduled.set(false);
66+
ch.flush();
67+
});
68+
} catch (RejectedExecutionException ignored) {
69+
flushScheduled.set(false);
70+
}
71+
}
72+
});
73+
} catch (RejectedExecutionException ignored) {
74+
// Event loop is shutting down; the channel is going away anyway.
75+
}
4776
}
4877

4978
@Override
@@ -63,7 +92,20 @@ public void onError(Throwable t) {
6392

6493
@Override
6594
public void onClose() {
66-
// disconnect from downstream server
67-
downstreamServerConn.close();
95+
// Flush before closing: deferred writes from onReceive() may still be
96+
// sitting in the channel's outbound buffer with the flush scheduled as
97+
// a separate EventLoop task, so closing without a final flush can drop
98+
// the last payload.
99+
Channel ch = downstreamServerConn;
100+
try {
101+
ch.eventLoop().execute(() -> {
102+
ch.flush();
103+
ch.close();
104+
});
105+
} catch (RejectedExecutionException ignored) {
106+
// Event loop already shut down: close directly. Netty's close is
107+
// thread-safe and a no-op on an already-closed channel.
108+
ch.close();
109+
}
68110
}
69111
}

core/src/main/java/com/minekube/connect/packet/PacketHandlersImpl.java

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,25 @@
2929
import com.minekube.connect.api.packet.PacketHandlers;
3030
import com.minekube.connect.api.util.TriFunction;
3131
import io.netty.channel.ChannelHandlerContext;
32-
import java.util.ArrayList;
3332
import java.util.Collection;
3433
import java.util.Collections;
35-
import java.util.HashMap;
36-
import java.util.HashSet;
3734
import java.util.List;
3835
import java.util.Map;
3936
import java.util.Set;
37+
import java.util.concurrent.ConcurrentHashMap;
38+
import java.util.concurrent.CopyOnWriteArrayList;
39+
import java.util.concurrent.CopyOnWriteArraySet;
4040
import lombok.AllArgsConstructor;
4141
import lombok.Getter;
4242

4343
public final class PacketHandlersImpl implements PacketHandlers {
44-
private final Map<PacketHandler, List<HandlerEntry>> handlers = new HashMap<>();
45-
private final Set<TriFunction<ChannelHandlerContext, Object, Boolean, Object>> globalPacketHandlers = new HashSet<>();
46-
private final Map<Class<?>, Set<TriFunction<ChannelHandlerContext, Object, Boolean, Object>>> packetHandlers = new HashMap<>();
44+
// CopyOnWriteArraySet for the per-class fanout: reads happen on every packet
45+
// (hot path, must be lock-free); writes only on register/deregister.
46+
private final Map<PacketHandler, List<HandlerEntry>> handlers = new ConcurrentHashMap<>();
47+
private final Set<TriFunction<ChannelHandlerContext, Object, Boolean, Object>> globalPacketHandlers =
48+
new CopyOnWriteArraySet<>();
49+
private final Map<Class<?>, Set<TriFunction<ChannelHandlerContext, Object, Boolean, Object>>> packetHandlers =
50+
new ConcurrentHashMap<>();
4751

4852
@Override
4953
public void register(
@@ -55,10 +59,10 @@ public void register(
5559
return;
5660
}
5761

58-
handlers.computeIfAbsent(handler, $ -> new ArrayList<>())
62+
handlers.computeIfAbsent(handler, $ -> new CopyOnWriteArrayList<>())
5963
.add(new HandlerEntry(packetClass, consumer));
6064

61-
packetHandlers.computeIfAbsent(packetClass, $ -> new HashSet<>(globalPacketHandlers))
65+
packetHandlers.computeIfAbsent(packetClass, $ -> new CopyOnWriteArraySet<>(globalPacketHandlers))
6266
.add(consumer);
6367
}
6468

@@ -70,7 +74,7 @@ public void registerAll(PacketHandler handler) {
7074

7175
TriFunction<ChannelHandlerContext, Object, Boolean, Object> packetHandler = handler::handle;
7276

73-
handlers.computeIfAbsent(handler, $ -> new ArrayList<>())
77+
handlers.computeIfAbsent(handler, $ -> new CopyOnWriteArrayList<>())
7478
.add(new HandlerEntry(null, packetHandler));
7579

7680
globalPacketHandlers.add(packetHandler);
@@ -88,13 +92,19 @@ public void deregister(PacketHandler handler) {
8892
List<HandlerEntry> values = handlers.remove(handler);
8993
if (values != null) {
9094
for (HandlerEntry value : values) {
91-
Set<?> handlers = packetHandlers.get(value.getPacket());
92-
93-
if (handlers != null) {
94-
handlers.removeIf(o -> o.equals(value.getHandler()));
95-
if (handlers.isEmpty()) {
96-
packetHandlers.remove(value.getPacket());
97-
}
95+
// registerAll() stores HandlerEntry with packetClass == null.
96+
// ConcurrentHashMap rejects null keys, so skip the per-class
97+
// lookup for global handlers (the old HashMap returned null
98+
// silently for the same case).
99+
Class<?> packetClass = value.getPacket();
100+
if (packetClass != null) {
101+
// computeIfPresent atomically removes the entry only if it's
102+
// still empty after our removal, so a concurrent register()
103+
// that re-populates the set in between isn't dropped.
104+
packetHandlers.computeIfPresent(packetClass, (k, set) -> {
105+
set.removeIf(o -> o.equals(value.getHandler()));
106+
return set.isEmpty() ? null : set;
107+
});
98108
}
99109

100110
globalPacketHandlers.remove(value.getHandler());

core/src/main/java/com/minekube/connect/platform/command/CommandUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public abstract class CommandUtil {
8080

8181
public @NonNull Collection<String> getOnlineUsernames() {
8282
Collection<String> usernames = new ArrayList<>();
83-
getOnlinePlayers().forEach(this::getUsernameFromSource);
83+
getOnlinePlayers().forEach(player -> usernames.add(getUsernameFromSource(player)));
8484
return usernames;
8585
}
8686

0 commit comments

Comments
 (0)