[#124] Create the RemoteRequest promise at construction so an unsent request can be cancelled - #127
Open
vharseko wants to merge 1 commit into
Conversation
…uction so an unsent request can be cancelled RemoteRequest was registered in RemoteConnectionGroup.remoteRequests before its promise existed: the promise was created inside the send function and reset to null when a send failed. Cancelling such a request - from WebSocketConnectionGroup.shutdown(), submitRequestCancel() or cancel() - threw NullPointerException from getPromise().cancel(...). On the client it escaped ClientRemoteConnectorInfoManager.doClose() after isRunning had been flipped, so the group, the private WebSocket connections and the close listeners (ConnectionManager's registry entry) were never cleaned up. The promise is now created in the constructor and getPromise() never returns null. The send function does not send a request whose promise is already done, so a request cancelled before it was sent hands its caller the cancelled promise instead of an answer that never arrives; a failed send keeps the same promise for the next connection. tryCancel(true) notifies the remote side only for a delivered message, and a cancel that races the send in progress is delivered after the request rather than ahead of it. Fixes OpenIdentityPlatform#124
| this.completionCallback = completionCallback; | ||
| this.promise = new PromiseImpl<V, E>() { | ||
|
|
||
| protected E tryCancel(boolean mayInterruptIfRunning) { |
|
|
||
| }; | ||
| this.promise.thenOnResultOrException(new Runnable() { | ||
| public void run() { |
| } | ||
| } | ||
| } | ||
| public Promise<V, E> apply(H value) throws Exception { |
|
|
||
| public Promise<V, E> apply(H value) throws Exception { | ||
| return resultPromise; | ||
| public Promise<V, E> apply(H remoteConnectionHolder) throws Exception { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #124
Problem
RemoteConnectionGroup.allocateRequestregisters aRemoteRequestinremoteRequestsbefore it is sent, butRemoteRequest.promiseonly came into existence inside the send function (getSendFunction()→apply(holder)) and was reset tonullwhen a send failed. For the whole window between registration and a successful sendgetPromise()returnednull, and every cancellation path dereferenced it unguarded:WebSocketConnectionGroup.shutdown(),RemoteConnectionGroup.submitRequestCancel,RemoteRequest.cancel(), plusRemoteOperationRequest.check()throughgetExceptionHandler().On the client the
NullPointerExceptionescapedClientRemoteConnectorInfoManager.doClose()afterConnectionPrincipal.close()had already flippedisRunning, so the group'sdelegate.close(), the private WebSocket connections,globalConnectionGroups.removeand the close listeners (ConnectionManager'sregistry.remove(info)) were all skipped, and a secondclose()was a no-op. The server side has the same loop inOpenICFWebSocketApplication.close()/OpenICFWebSocketCreator.close(), where the NPE would stop the remaining groups from shutting down.The window became reachable from user code with #104, which moved the initial
CONNECTOR_INFOrequest fromhandshake()(beforeconnectPromiseresolves) tohandshakeComplete()(after it):connect().getOrThrow()now returns while that request is registered but not yet sent, andclose()from the caller's thread runs concurrently with the send.Change
RemoteRequestonly:final;getPromise()never returnsnull. The completion callback (removal fromremoteRequests) is registered there too -PromiseImplfires it on cancellation as well, so a request cancelled before it was sent unregisters itself.trySubmitRequestreturns the request,getOrThrowthrows the cancellation exception) instead of an answer that can never arrive.trySendMessageretries on its next connection with the same promise.tryCancel(true)sendsCancelOpRequestonly for a delivered message, so a request cancelled before the send puts nothing on the wire. A cancel racing a send in progress is caught by a store-then-load pair on both sides (remoteCancelRequested/requestTime, deduplicated byremoteCancelSent): the cancel message is sent after the request, once, instead of ahead of it.tryLocknot acquired within a minute now throwsIllegalStateExceptioninstead of returning an unsent promise.Tests
RemoteRequestCancelBeforeSendTest(connector-framework-rpc), deterministic through a request that blocks increateMessageElementand a holder that blocks insendString:promiseExistsWhileRequestIsRegisteredButNotSent,cancelBeforeSendCancelsPromiseWithoutSendingAnything(viasubmitRequestCancel),groupShutdownBeforeSendCancelsPromiseWithoutSendingAnything(theshutdown()loop) andcancelDuringSendNotifiesRemoteAfterTheRequestIsDeliveredfailed before the change - the first three with the exact NPE /nullfrom the issue, the last one with the cancel message overtaking the request.cancelAfterSendNotifiesRemoteandsendFailsOverToTheNextConnectionAndCompletesNormallypin down behaviour that had to survive the change; they pass before and after.Local runs: connector-framework-rpc 19 tests, connector-framework-server 29, connector-server-grizzly 34, connector-server-jetty 34, all green.
Compatibility
getPromise()of a registered request is a pending promise before the send rather thannull; a request cancelled before its send is returned bytrySubmitRequestwith a cancelled promise rather than sent. Both are visible only to code that raced the send, which previously got the NPE.Noticed, not changed
handshakeComplete()retries the initialCONNECTOR_INFOrequest on any failure, including the cancellation fromshutdown(), and the retry goes out over the still-open socket beforeprincipalIsShuttingDown()removes it. Pre-existing and independent of this change: handshakeComplete() resends the initial CONNECTOR_INFO request while the group is shutting down #125.WebSocketConnectionGroup.principalsis a plainTreeSetwritten from handshake threads and fromprincipalIsShuttingDown()/close()without synchronisation.