Commit 7492d5d
authored
feat(transport-tcp): replace NIO selector with per-connection virtual-thread blocking I/O (#2612)
* feat(transport-tcp): replace NIO selector with per-connection virtual-thread blocking I/O
Each connection runs a blocking SocketChannel.read() loop on its own virtual thread
instead of a per-connection NIO Selector. On Java 21 blocking-mode reads/writes park the
virtual thread and release the carrier, so the selector (which pins the carrier in
select()) and the OP_WRITE + Thread.sleep(1) write busy-wait are removed. A full ring
buffer applies backpressure (park-and-retry) instead of toggling OP_READ.
Public surface, readLock, RingBuffer, and the AsyncTransportInstance callback contract are
unchanged: the existing TcpTransportInstanceTest (31 tests) passes unmodified.
Scaling probe (TcpTransportInstanceScalingTest): 200 idle connections use 2 carrier
threads with the blocking model vs 201 with the selector model.
* fix(transport-tcp): emit close log/audit only on successful close
Move the "TCP connection closed" debug line and CLOSE audit event out of
the finally block and into the success path of close(). Previously they
ran even when socketChannel.close() threw and the method rethrew, so a
failed close logged both an ERROR audit event and a misleading CLOSE
"Closed" event. The readThread.join() stays in finally so the read loop
is always awaited. Also correct an inaccurate comment in the scaling
test (the stop-flag holder is a field, not a way to avoid one).
* fix(transport-tcp): drop dead write -1 check and tidy audit/test nits
- write(): blocking SocketChannel.write() never returns -1 (that signals
read EOF), so the `written == -1` branch was dead code. A broken or closed
connection already surfaces as IOException/AsynchronousCloseException, both
handled below. Remove the check.
- constructor: errorMsg already embeds e.getMessage(), so the second ERROR
audit event duplicated the first. Emit a single event.
- constructor: start the read-loop virtual thread last (after the INFO log
and CONNECT audit), so an unchecked throw from logging/audit cannot leak an
already-running read thread and the open SocketChannel — the catch only
handles IOException and does not stop the read loop.
- close(): skip readThread.join() when close() runs on the read thread itself
(a disconnect/data listener calling close()), since joining yourself only
stalls for the timeout and the loop already exits once open is false.
- scaling test: take one Thread.getAllStackTraces() snapshot so carriers and
total are counted from the same instant instead of two separate calls.
- scaling test: exclude ForkJoinPool.commonPool workers from the carrier
count so unrelated parallel-stream workers cannot inflate it.1 parent 1b96138 commit 7492d5d
2 files changed
Lines changed: 253 additions & 203 deletions
File tree
- plc4j/transports/tcp/src
- main/java/org/apache/plc4x/java/transport/tcp
- test/java/org/apache/plc4x/java/transport/tcp
0 commit comments