Close handed-off backend candidate on pipeline error

This commit is contained in:
layue13 committed 2026-09-25 20:36:27 +08:00
1 parent d90ebf903e
commit 86a3fad5b8
4 files changed
+57 -1

No files matched your search

+2
View File
@@ -93,6 +93,8 @@ plugins:
当前提交的多轮、同条件本机测量及原始输出见 [生命周期修复后的会话基准记录](benchmarks/results/2026-09-25-session-after-lifecycle.md)。之前提交的测量见 [原会话基准记录](benchmarks/results/2026-09-25-current-session-benchmark.md)。这些是代理自身的合成流量性能验证,不是后端负载观测。
当前会话路径的 [JFR 采样记录](benchmarks/results/2026-09-25-session-jfr-screening.md) 只提供了分配线索,代理 I/O 线程的 CPU 执行样本不足以定位热点;因此没有据此改动普通转发路径。
将 `-Window` 改为 `16` 可测每连接最多 16 个在途往返的情形。
原 stop-and-wait 小样本见 `benchmarks/results/proxy-session-benchmark-smoke-2026-09-25.md`;Window 参数的小样本和 Window=1/16 重复测量见 `benchmarks/results/2026-09-25-proxy-session-window.md`。这些是环回网络上的合成帧对照,不能代表 Forge 整合包、真实后端或跨主机部署的性能。
@@ -0,0 +1,16 @@
# JFR screening of the session hot path
Run date: 2026-09-25 (Asia/Shanghai). Profiled revision: `d90ebf9` on Windows 11 Enterprise, AMD Ryzen 7 7800X3D, Zulu OpenJDK 25.0.4.1. The recording ran for 44 seconds with Java Flight Recorder's `profile` settings. The local JFR file is under ignored `benchmarks/build/`; it is not published because JFR includes process environment events.
Command, after compiling `benchmarks/ProxySessionBenchmark.java` and installing the distribution:
```powershell
java -XX:StartFlightRecording=filename=benchmarks/build/session-w1.jfr,settings=profile,dumponexit=true `
-cp "benchmarks/build;proxy-core/build/install/strataproxy/lib/*" `
dev.strataproxy.core.session.ProxySessionBenchmark `
--connections 4 --messages 40000 --warmup 4000 --payload 4096 --repeats 5 --window 1
```
All direct and proxy echoes were correct with zero errors. The full recording contained 11,845 `jdk.ObjectAllocationSample` events and 167 `jdk.ExecutionSample` events. In the full recording, the benchmark client's `readFrame` byte-array allocation accounted for 82.97% of weighted allocation pressure. After filtering to `strataproxy-session-io-*` threads, 2,172 allocation samples remained, but no execution samples. Weighted I/O-thread allocation pressure was led by `DirectByteBuffer.duplicate` (34.01%) and `DirectByteBuffer.slice` (25.68%); `Integer.valueOf` (6.90%) sampled under the Windows selector's `WEPollSelectorImpl`.
These are sampled allocation-pressure shares, not exact allocated bytes or a per-packet cost. The recording combines direct and proxy phases, and JFR did not capture enough proxy I/O execution samples to locate a CPU bottleneck. It therefore does not justify changing the ordinary relay path. The earlier [unprofiled paired run](2026-09-25-session-after-lifecycle.md) remains the throughput reference for this revision family. Profile again with representative Forge traffic and a method that yields usable CPU samples before attributing a production bottleneck.
@@ -113,7 +113,8 @@ final class TransferCandidate extends ChannelInboundHandlerAdapter {
}
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
fail(ctx, "replacement backend failed");
if (state == State.HANDED_OFF) ctx.close();
else fail(ctx, "replacement backend failed");
}
Minecraft1710PlayPackets.JoinGame joinGame() { return joinGame; }
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.nio.charset.StandardCharsets;
@@ -17,6 +18,42 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class TransferCandidateTest {
@Test
void errorAfterHandoffClosesTheCandidateChannel() {
UUID playerId = UUID.randomUUID();
var ready = new AtomicBoolean();
var candidate = new TransferCandidate(playerId, "Player", new TransferCandidate.Listener() {
@Override public void ready(TransferCandidate ignored) { ready.set(true); }
@Override public void failed(TransferCandidate ignored, String reason) {
throw new AssertionError("candidate has already been handed off");
}
});
var channel = new EmbeddedChannel(candidate);
try {
ByteBuf login = Unpooled.buffer();
try {
ProtocolVarInt.write(login, 2);
writeString(login, playerId.toString());
writeString(login, "Player");
channel.writeInbound(Minecraft1710PlayPackets.frame(channel.alloc(), login));
} finally { login.release(); }
ByteBuf join = Unpooled.buffer();
try {
ProtocolVarInt.write(join, Minecraft1710PlayPackets.JOIN_GAME);
join.writeInt(200).writeByte(0).writeByte(0).writeByte(1).writeByte(20);
writeString(join, "default");
channel.writeInbound(Minecraft1710PlayPackets.frame(channel.alloc(), join));
} finally { join.release(); }
assertTrue(ready.get());
candidate.handOff();
channel.pipeline().fireExceptionCaught(new IllegalStateException("backend read failed"));
assertFalse(channel.isOpen());
} finally {
candidate.close();
channel.finishAndReleaseAll();
}
}
@Test
void boundsTinyQueuedPacketsAndReleasesTheirBuffersOnFailure() {
UUID playerId = UUID.randomUUID();