Fix lost pre-handshake responses causing testAsynchronousBatch to hang#108
Conversation
Messages are dispatched to a thread pool in OpenICFServerAdapter, so an operation response arriving on a freshly opened socket could be processed before that socket's handshake and was silently discarded. For a batch operation this lost the batch-token response, and CompletionListener then timed out without completing the promise, leaving executeBatch() blocked until the connection group checker failed it 9 minutes later with "Operation finished on remote server with unknown result". Wait briefly for the in-flight handshake before dispatching non-handshake messages instead of dropping them, and make CompletionListener fail the promise when no batch token arrives so a lost token surfaces as an error instead of a hang. Fixes OpenIdentityPlatform#107
maximthomas
left a comment
There was a problem hiding this comment.
Diagnosis is right and both fixes improve on the status quo. Three things before merge:
1. Missing tryCancelRemote on the new failure path. The sibling error path at BatchApiOpImpl.java:429-434 pairs the two; without it the remote batch runs to completion with nobody listening.
getExceptionHandler().handleException(new ConnectorException(
"Batch operation failed to return a batch token"));
tryCancelRemote(getConnectionContext(), getRequestId());2. volatile on the fields the new code depends on. awaitHandshake polls isHandHooked() → getRemoteConnectionContext(), which is a non-volatile field in all three holders (OpenICFWebSocketApplication.java:172, ClientRemoteConnectorInfoManager.java:506, jetty SinglePrincipal). Thread.sleep keeps the loop from being hoisted, but the RemoteOperationContext itself isn't safely published. Same for BatchApiOpImpl.java:293 returnToken — written on a message thread, read by CompletionListener. A stale read there now produces a spurious ConnectorException instead of a hang.
3. Shorter timeout, and exempt error responses. The real race resolves in tens of milliseconds, so 10 s buys nothing but costs a pool thread per message on sockets that will never hand-hook (messageExecutor is an unbounded cached pool — ConnectorFramework.java:83). It also delays legitimate "received request before handshake" feedback from immediate to 10 s. Error responses are handled at OpenICFServerAdapter.java:177, before the isHandHooked() guard, so they never needed the handshake:
private static final long HANDSHAKE_WAIT_MS = 500;
if (!isHandshakeMessage(message) && !message.getResponse().hasError()) {
awaitHandshake(socket, message.getMessageId());
}Minor: the give-up branch logs at info; warn matches the neighbouring drop paths at lines 215/221.
On tests — the adapter race isn't reproducible on demand, agreed. But the BatchApiOpImpl half needs no race: start CompletionListener, never deliver a token, assert executeBatch throws within ~5 s and that onError fired while onCompleted did not.
Bigger picture: this synchronizes only against the handshake boundary — two operation responses on the same socket can still be reordered. Serializing dispatch per socket would kill the bug class and make awaitHandshake unnecessary. Follow-up, not a blocker.
- Cancel the remote batch operation when CompletionListener times out without a token, mirroring the sendResponse failure path; guard with promise.isDone() and tolerate an already-dead transport. - Make the RemoteOperationContext fields volatile in the client, grizzly and jetty connection holders: they are written on the handshake- processing thread and read from other pool threads via isHandHooked(). - Shorten the awaitHandshake wait from 10s to 500ms, skip it for error responses (handled before the isHandHooked guard) and log the give-up at warn instead of info. - Add BatchApiOpImplTest covering the lost-token case: executeBatch now fails with ConnectorException in ~5s, observer.onError fires and onCompleted does not.
|
Thanks for the thorough review — all points addressed in 2efeda5: 1. 2. 3. Timeout / error responses — wait shortened to Test — added Bigger picture — agreed that serializing dispatch per socket would eliminate the whole reordering class and make Test results: |
Fixes #107
AsyncRemoteSecureConnectorInfoManagerTest.testAsynchronousBatchintermittently fails in CI after exactly 540 s withConnectorException: Operation finished on remote server with unknown result(example run). This still reproduced after #105, which fixed a different race.Root cause
OpenICFServerAdapter.onMessagedispatches every WebSocket message to a cached thread pool, so two messages delivered in order on the same socket can be processed out of order. On a freshly opened socket the batch-token operation response overtook the handshake response, and the pre-handshake branch ofhandleResponseMessagesilently discarded it (Client received response('2') before handshake via socket:...).BatchApiOpImpl$InternalRequest.CompletionListenertimed out withreturnToken == nulland exited without completing the promise, leaving the caller blocked forever inpromise.getOrThrowUninterruptibly()insideexecuteBatch().inconsistent()strikes) finally failed the operation at the 9-minute mark — hence exactly 540 s.Fix
OpenICFServerAdapter: before dispatching a non-handshake message on a socket that is not yet handhooked, briefly wait (poll up to 10 s) for the concurrently-processing handshake to land instead of discarding the message. In the race this resolves in tens of milliseconds; if no handshake ever arrives, behavior falls through to the existing drop-with-error path.BatchApiOpImpl: whenCompletionListenertimes out with no batch token, fail the promise with aConnectorExceptioninstead of returning silently, so any future message loss surfaces as an error in ~5 s instead of a 9-minute hang.observer.onCompleted()is now only called when a token was actually delivered; the failure path signalsobserver.onErrorvia the existingthenOnExceptionchain.Testing
AsyncRemoteSecureConnectorInfoManagerTest(the flaky class): 14/14 passconnector-server-grizzlysuite: 32/32 passconnector-framework-serversuite: 24/24 passThe race itself is timing-dependent (not reproducible locally on demand), so the real proof will be Windows CI over time.