Repository navigation
[#1180] Report the fail-over failure of the Grizzly proxy listener tests instead of timing out - #1181
Conversation
…ly proxy listener tests instead of timing out A failed connection inside the overridden handleAccept left the context the test waits for unresolved, so any failure showed up as a bare TimeoutException after 10 seconds, as on the Windows CI run. Resolve that context with the failure, and wait longer than the default connect timeout of the two nested connections. The bind variant failed the bind with a plain result, which the listener cannot encode as a bind response: it closed the connection and the cause was lost. Fail it with a bind result. Both tests now share the offline-then-online fail-over, which closes its connection factories and connects to the numeric loopback address; the client factories are closed as well.
maximthomas
left a comment
There was a problem hiding this comment.
praise: The change sits exactly where the CI failure lost its cause.
proxyServerConnection.context.handleException(e)in thehandleAcceptoverride (:594) turns the bareTimeoutExceptionof the CI run into the actual fail-over failure.handleBindnow fails withResponses.newBindResult(ResultCode.OTHER)(:666), which removes theResultImpl→BindResultClassCastExceptionthat left the bind unanswered.failOverToOnlineServerclosesofflineFactoryandonlineFactoryinfinally(:229,:235), which BASE leaked.
issue (non-blocking): The bind variant still does not get the nested cause to the client.
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java:666-667
The diagnostic message is only the wrapper text ("Unexpected exception when connecting to online server"). setCause(e) stays on the server side, because LDAPWriter.writeResultHeader encodes the result code, matched DN, diagnostic message and referrals, never the cause, and the class logs at SEVERE. When a nested connect in testLDAPListenerProxyDuringHandleBind fails, the report says which leg failed but not whether it was refused, timed out or unresolved. The "succeeded unexpectedly" arm, the one used in the PR's verification, explains itself.
} catch (final LdapException e) {
// A bind must fail with a bind result: the listener cannot encode any other
// result as a bind response, and would then never answer the bind.
final Throwable cause = e.getCause();
final String message = cause != null
? e.getResult().getDiagnosticMessage() + ": " + cause
: e.getResult().getDiagnosticMessage();
resultHandler.handleException(newLdapException(Responses.newBindResult(ResultCode.OTHER)
.setDiagnosticMessage(message).setCause(e)));
}issue (non-blocking): A RuntimeException from the fail-over helper escapes handleAccept without resolving the context.
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java:588-596, :213
The override catches only LdapException. findFreeSocketAddress() wraps an IOException in a RuntimeException and is called outside any try in the helper. The factory constructors and the close() calls in its finally blocks are not wrapped either. When one of them throws, proxyServerConnection.context stays unresolved and the test again waits out context.get(30, SECONDS) with a bare TimeoutException. The comment at :591-593 promises a report for any handleAccept failure. The only realistic trigger is a failed loopback port-0 bind.
} catch (final LdapException e) {
proxyServerConnection.context.handleException(e);
throw e;
} catch (final RuntimeException e) {
proxyServerConnection.context.handleException(newLdapException(ResultCode.OTHER, e));
throw e;
}issue (non-blocking): Switching the offline address to getHostString() changes nothing: the host is still "localhost", not numeric.
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java:212-215
findFreeSocketAddress() returns ServerSocket.getLocalSocketAddress() of a socket bound to InetAddress.getLoopbackAddress(). That address keeps its preset host name, "localhost". So getHostString() returns the same string BASE's getHostName() did, and neither call ever did a lookup. A probe printed hostString=localhost on JDK 8u362, 11.0.25, 17.0.11, 21.0.2 and 26, and with -Djava.net.preferIPv6Addresses=true too. The offline connect still resolves "localhost" forward. The comment and the PR body ("connects to the numeric loopback address") tell the next reader that road is closed.
// The numeric host avoids a forward lookup of "localhost".
final InetSocketAddress offlineAddress = findFreeSocketAddress();
final LDAPConnectionFactory offlineFactory =
new LDAPConnectionFactory(offlineAddress.getAddress().getHostAddress(), offlineAddress.getPort());Or: keep getHostName(), drop the comment, and correct the sentence in the commit body.
suggestion (non-blocking): Use getHostString() on the listener addresses. Those are the legs that do a reverse lookup.
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java:221, :610, :679
LDAPListener.firstSocketAddress() is the Grizzly channel's getLocalAddress(), which has no host name (hostString=127.0.0.1 measured with Grizzly 2.3.35 on JDK 8/11/17/26). So onlineAddress.getHostName() reverse-resolves 127.0.0.1 inside handleAccept, before the context resolves. That adds time to the 30 s window, which the new comment budgets as two connect timeouts only. A stalled resolver would bring back the bare timeout. This road predates the PR.
new LDAPConnectionFactory(onlineAddress.getHostString(), onlineAddress.getPort());
// and in both tests
new LDAPConnectionFactory(proxyAddr.getHostString(), proxyAddr.getPort());…zzly proxy listener tests with its cause An unchecked exception from the fail-over, such as a failed loopback bind in findFreeSocketAddress, still escaped both overrides without an answer: handleAccept left the context unresolved, so the test waited out 30 seconds for a bare TimeoutException, and handleBind left the bind unanswered, so the client saw only "Server Connection Closed". Report it like the expected failures. A bind result reaches the client with its diagnostic message only, never with its cause: append the nested cause to that message, so that a failed connection says whether it was refused, timed out or not resolved. The offline host was not numeric: the address of the reserved socket keeps the host name "localhost", which getHostString returns as well. Connect to its numeric address instead. The listener addresses have no host name, so getHostName reverse-resolved them, inside handleAccept for the online server: use getHostString for them.
|
Thanks, all four points hold. Addressed in afbe983. 1. Bind cause. Confirmed: 2. 3. Offline host. You are right, and my sentence was wrong: 4. Listener addresses. Confirmed: on the channel address The |
maximthomas
left a comment
There was a problem hiding this comment.
praise: All four round-1 points are fixed where they occur, and CI at afbe983 is green on every build-maven cell, windows-latest 11 and 26 included.
offlineAddress.getAddress().getHostAddress()(:215) makes the "numeric host" comment true, and the online leg and both proxy clients usegetHostString()of listener addresses (:221,:614,:691). None of them does a name lookup inside the 30 s window any more.catch (final RuntimeException e)in both overrides (:596,:677) means an unchecked failure now resolves the waiting context or answers the bind, instead of ending in a timeout with no cause.- The bind diagnostic message now appends the nested cause (
:672-674). The message is the only part of the result thatLDAPWriterencodes.
Fixes #1180
Problem
GrizzlyLDAPListenerTestCase#testLDAPListenerProxyDuringHandleAccepttimed out onwindows-latest/ JDK 26 with a bareTimeoutExceptionat the first wait (line 605).The overridden
handleAcceptconnects to an offline port and then to the online listener, and resolves the proxy'scontextonly after both succeed. It runs synchronously on a selector thread of the server transport. Any exception before the last line therefore left thatcontextunresolved. The causes could be: the offline connection unexpectedly succeeding, aTimeoutResultException(which is not aConnectionException), a failed online connection, or an unchecked exception such as a failed loopback bind infindFreeSocketAddress(). In each case the listener only closed the client connection, and the test waited out its 10 seconds without a cause. The class runs with logging atSEVERE, so the job log shows nothing either. Every way the fail-over can fail looks the same as the CI failure, so the run cannot tell us which one happened.testLDAPListenerProxyDuringHandleBindhad the same blind spot in a different form. It failed the bind with a plainResult, which the listener cannot encode as a bind response (ClassCastException: ResultImpl cannot be cast to BindResultinside Grizzly). The connection was closed and the client saw onlyServer Connection Closed, or the test hung until itstimeOut.Fix (test only)
handleAcceptresolves the proxy'scontextwith the failure before rethrowing it, unchecked exceptions included, so the assert fails at once with the actual cause.handleAcceptmakes two nested connections before it resolves the context, and each may take up to the default connect timeout (10 s). A shorter wait would again expire before a failed attempt can report itself. Because a failure is now reported as soon as it happens, the longer window does not hide anything.BindResult, for unchecked exceptions too. Only the diagnostic message of that result reaches the client, never its cause, so the message carries the nested cause.failOverToOnlineServerhelper. It closes the offline and online connection factories (previously leaked). It connects to the numeric address of the offline port: the address of the reserved socket keeps the host namelocalhost, so neithergetHostName()norgetHostString()gives a numeric host.getHostName()reverse-resolved them, for the online server insidehandleAccept, outside the connect timeout. They are now used throughgetHostString().finally. In...DuringHandleAccept,connection.close()moved intofinally, theisClosedwait is bounded, and a@Test(timeOut = 60000)was added. ThetimeOutof...DuringHandleBindgoes from 10 s to 60 s, for the same reason as the longer wait.The root cause on Windows is still unknown: it cannot be reproduced locally. This change makes the next occurrence name it. I also tried reserving the offline port with a socket that is bound but not listening, to rule out the port being reused. On macOS such a port does not refuse connections: the connect times out instead. So the offline port is still picked with
findFreeSocketAddress().Verification
Each mutant was run against both tests, before (BASE of the PR) and after:
TimeoutExceptionafter 11.2 s; bind:Server Connection Closed,ClassCastExceptionin the logConnection to offline server succeeded unexpectedlyConnection refusedin the cause chain; bind fails in 0.4 s withUnexpected exception when connecting to online server: ...ConnectionException: Connect Error: Connection refusedRuntimeExceptionat the start of the helpermutant: loopback bind failed; bind fails in 0.5 s withjava.lang.RuntimeException: mutant: loopback bind failedAgainst the first commit of this PR, the last two mutants gave: a bind message without the cause (
Unexpected exception when connecting to online server); and for theRuntimeException, a bareTimeoutExceptionafter 31 s in the accept test andServer Connection Closedin the bind test.Without a mutant: all of
opendj-grizzlypasses, 1040 tests in 9 classes with 0 failures (mvn verify, checkstyle included).