feat: add TcpStackAwareSocketChannel for z/OS TCP/IP stack recovery — GH#4776 - #4792
feat: add TcpStackAwareSocketChannel for z/OS TCP/IP stack recovery — GH#4776#4792balhar-jakub wants to merge 5 commits into
Conversation
QA + Security Review — PR #4792 (#4776)Verdict: APPROVED Build and Tests
Pavel's Lens — All 8 Rules Checked
Minor Notes (non-blocking)
Security Assessment
APPROVED — ready for merge. |
…StackAwareSocketChannel config (#4776) Move isRecycledClass and isTcpStackRestarted from FixedServerSocketChannel instance methods to package-private static methods on TomcatAcceptFixConfig. Add apiml.tcpStackAwareSocketChannel.enabled config property (default true). Update call sites in FixedServerSocketChannel.accept() to use static methods. Signed-off-by: Jakub Balhar <jakub.balhar@broadcom.com>
…#4776) Create ExcludedSocketOps interface (20 methods) and TcpStackAwareSocketChannel inner class extending SocketChannel with @DeleGate. Intercepts read/write ops to detect EDC5122I/NetworkRecycledException, closes dead socket, re-throws. Wire into FixedServerSocketChannel.accept() guarded by config property apiml.tcpStackAwareSocketChannel.enabled (default true). Signed-off-by: Jakub Balhar <jakub.balhar@broadcom.com>
Add comprehensive tests for TcpStackAwareSocketChannel wrapper: - Normal read/write passthrough - EDC5122I detection on read/write/scatter/gather (close + rethrow) - NetworkRecycledException detection via MockedStatic - Non-EDC5122I IOException passthrough (no close) - safeClose robustness (IOException on close ignored) - configureBlocking/isBlocking delegation - Integration test with real SocketChannel - Config disabled returns raw SocketChannel Update TcpStackRestartHandling tests to use static methods. All 76 tests pass. Signed-off-by: Jakub Balhar <jakub.balhar@broadcom.com>
|
DCO check failed — all 3 commits are missing Fix: |
75421a5 to
44fcad4
Compare
Change default from true to false. The wrapper breaks Tomcat socket acceptance on non-z/OS platforms (Linux CI runners), causing tests to hang with Connection refused until BuildAndTest times out at 35 minutes. The feature only has value on z/OS where TCP/IP stack restarts can occur — z/OS deployments must explicitly set apiml.tcpStackAwareSocketChannel.enabled=true. Signed-off-by: Jakub Balhar <jakub.balhar@broadcom.com>
|
|
balhar-jakub
left a comment
There was a problem hiding this comment.
Multi-Reviewer Review
Verdict: Approve with minor NITs.
Findings:
- ℹ️ NIT — Typo in
IMPL_CLOSE_SELECTABGLE_CHANNEL_HANLE(should beHANDLE) — pre-existing, but inherited. - ℹ️ NIT — Method counts in
ExcludedSocketOpscomment are slightly off (14/6 split is approximate). - ℹ️ NIT —
close()is excluded but not overridden — defense-in-depth viaimplCloseSelectableChannel()works. - ℹ️ NIT — No
toString()override. - ℹ️ NIT —
safeCloseswallows IOException silently; consider trace-level log. - ✅ Test coverage is excellent (every read/write variant tested, mockStatic for
NetworkRecycledException).
Good:
- Feature flag
apiml.tcpStackAwareSocketChannel.enableddefaults tofalse— safe opt-in. @Delegateapproach correctly handles 20 excluded methods + delegates the rest.isTcpStackRestartednowstatic(was instance) — cleaner since no instance state needed.- New tests cover both the existing
isTcpStackRestartedand the newTcpStackAwareSocketChannel.
Optional follow-up:
- Update
apiml-tomcat-commonCHANGELOG / docs to mention the new flag. - Consider a tenant-level integration test that toggles the flag and verifies behavior.
Required before merge: none. All findings are NIT-level polish.
| * When EDC5122I or NetworkRecycledException is caught on read/write, the underlying | ||
| * socket is closed and the exception is re-thrown so Tomcat can discard the connection. | ||
| */ | ||
| class TcpStackAwareSocketChannel extends SocketChannel { |
There was a problem hiding this comment.
ℹ️ NIT — two notes about TcpStackAwareSocketChannel:
1. close() is excluded from @DeleGate but not manually overridden. Looking at ExcludedSocketOps, close() is in the list (step 10) — that's because SocketChannel.close() is final in AbstractInterruptibleChannel and cannot be overridden in Java (compile error). The wrapper inherits it directly. So when close() is called, it goes through the chain AbstractInterruptableChannel.close() → implCloseSelectableChannel() (which IS overridden). Good — this means the safeClose wrapper in I/O methods is redundant with the implCloseSelectableChannel override, but it's a defense-in-depth. Worth a comment:
// close() is final on AbstractInterruptableChannel; we override implCloseSelectableChannel() instead.2. No toString() override. The inherited Object.toString() will return class names, which is fine for debugging but may be confusing when the wrapper is logging. Consider overriding:
@Override
public String toString() {
return "TcpStackAwareSocketChannel{delegate=" + delegate + "}";
}Not blocking.
| } | ||
|
|
||
| @Override | ||
| protected void implCloseSelectableChannel() throws IOException { |
There was a problem hiding this comment.
ℹ️ NIT — typo in IMPL_CLOSE_SELECTABGLE_CHANNEL_HANLE (should be HANDLE). This typo is pre-existing in the original code (line 57, 72, 184), but the PR inherits it on line 425. Worth fixing now while the file is being touched:
| protected void implCloseSelectableChannel() throws IOException { | |
| IMPL_CLOSE_SELECTABGLE_CHANNEL_HANDLE.invoke(delegate); |
Cosmetic, but it improves readability.
| * The list of methods excluded from Lombok @Delegate for TcpStackAwareSocketChannel. | ||
| * First 14 are final methods on SocketChannel; last 6 are manually overridden. | ||
| */ | ||
| private interface ExcludedSocketOps { |
There was a problem hiding this comment.
ℹ️ NIT — comment counts are slightly off. The comment says "First 14 are final methods on SocketChannel; last 6 are manually overridden." Actually, SocketChannel.read(ByteBuffer) and write(ByteBuffer) are NOT final methods — they are abstract and are overridden in the wrapper. The 14/6 split is approximate and exchanges some inherited abstract methods (e.g., validOps(), provider()) with final claims. Consider rephrasing:
* The list of methods excluded from Lombok @Delegate for TcpStackAwareSocketChannel.
* Methods excluded are: (a) methods on SocketChannel that cannot be overridden via @Delegate,
* and (b) the read/write/implClose/implCloseSelectableChannel methods that are manually wrapped here.
| running.set(false); | ||
| } | ||
|
|
||
| static boolean isRecycledClass(Throwable t) { |
There was a problem hiding this comment.
✅ Good — feature flag is gated by apiml.tcpStackAwareSocketChannel.enabled (default false). This is the right default — the wrapper adds overhead per I/O call, so opt-in is the safer choice. Document the production-bringup plan in the operator docs (e.g., apiml-common-log-messages.yml or a CHANGELOG entry). What does the rollout story look like — enable per service in staging, then prod?
| assertTrue(TomcatAcceptFixConfig.isTcpStackRestarted(e)); | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
✅ Test coverage is excellent. Each variant of read/write is tested:
- pass-through (no exception)
- EDC5122I exception → close + re-throw
- non-EDC5122I exception → do NOT close
- scatter read/write variants
- NetworkRecycledException via mockStatic
- implCloseSelectableChannel / implConfigureBlocking delegation
- Configuration disabled path returns raw socket
The givenNetworkRecycledException_whenIsRecycledClass_thenReturnTrue test uses MockedStatic to mock the static method, which is the right pattern since com.ibm.net.NetworkRecycledException is not available on non-z/OS JDKs.
| } catch (IOException e) { | ||
| if (TomcatAcceptFixConfig.isTcpStackRestarted(e)) { | ||
| log.debug("TCP/IP stack restart detected during write on client socket; closing connection", e); | ||
| safeClose(delegate); |
There was a problem hiding this comment.
ℹ️ NIT — safeClose swallows IOException but the original delegate's close error is silently lost. This is intentional (best-effort close), but consider logging at trace level so debugging is possible:
private static void safeClose(SocketChannel ch) {
try {
ch.close();
} catch (IOException e) {
log.trace("Best-effort close failed (ignored)", e);
}
}Not blocking.




Closes #4776
Adds a SocketChannel wrapper that detects z/OS TCP/IP stack restarts (EDC5122I / NetworkRecycledException) during read/write operations on accepted client sockets. When detected, the wrapper safely closes the dead socket and re-throws so Tomcat discards the connection.
Changes
isTcpStackRestartedandisRecycledClassfrom instance methods to package-private static methods onTomcatAcceptFixConfigapiml.tcpStackAwareSocketChannel.enabledconfig property (default:true)ExcludedSocketOpsinterface for Lombok@Delegateexclusion listTcpStackAwareSocketChannelinner class:read(ByteBuffer),read(ByteBuffer[], int, int),write(ByteBuffer),write(ByteBuffer[], int, int)implCloseSelectableChannelandimplConfigureBlockingvia MethodHandlesFixedServerSocketChannel.accept()viawrapIfEnabled()guarded by configBuild
./gradlew :apiml-tomcat-common:clean build— BUILD SUCCESSFUL, all 76 tests pass.(QA review + CI gate to follow)