Skip to content

[#124] Create the RemoteRequest promise at construction so an unsent request can be cancelled - #127

Open
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issue-124-remote-request-promise
Open

vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issue-124-remote-request-promise

Conversation

@vharseko

Copy link
Copy Markdown
Member

Fixes #124

Problem

RemoteConnectionGroup.allocateRequest registers a RemoteRequest in remoteRequests before it is sent, but RemoteRequest.promise only came into existence inside the send function (getSendFunction()apply(holder)) and was reset to null when a send failed. For the whole window between registration and a successful send getPromise() returned null, and every cancellation path dereferenced it unguarded: WebSocketConnectionGroup.shutdown(), RemoteConnectionGroup.submitRequestCancel, RemoteRequest.cancel(), plus RemoteOperationRequest.check() through getExceptionHandler().

On the client the NullPointerException escaped ClientRemoteConnectorInfoManager.doClose() after ConnectionPrincipal.close() had already flipped isRunning, so the group's delegate.close(), the private WebSocket connections, globalConnectionGroups.remove and the close listeners (ConnectionManager's registry.remove(info)) were all skipped, and a second close() was a no-op. The server side has the same loop in OpenICFWebSocketApplication.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_INFO request from handshake() (before connectPromise resolves) to handshakeComplete() (after it): connect().getOrThrow() now returns while that request is registered but not yet sent, and close() from the caller's thread runs concurrently with the send.

Change

RemoteRequest only:

  • The promise is created in the constructor and is final; getPromise() never returns null. The completion callback (removal from remoteRequests) is registered there too - PromiseImpl fires it on cancellation as well, so a request cancelled before it was sent unregisters itself.
  • The send function does not send a request whose promise is already done. A request cancelled before the send hands its caller the cancelled promise (trySubmitRequest returns the request, getOrThrow throws the cancellation exception) instead of an answer that can never arrive.
  • A failed send propagates without resetting anything; the group's trySendMessage retries on its next connection with the same promise.
  • tryCancel(true) sends CancelOpRequest only 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 by remoteCancelSent): the cancel message is sent after the request, once, instead of ahead of it.
  • tryLock not acquired within a minute now throws IllegalStateException instead of returning an unsent promise.

Tests

RemoteRequestCancelBeforeSendTest (connector-framework-rpc), deterministic through a request that blocks in createMessageElement and a holder that blocks in sendString:

  • promiseExistsWhileRequestIsRegisteredButNotSent, cancelBeforeSendCancelsPromiseWithoutSendingAnything (via submitRequestCancel), groupShutdownBeforeSendCancelsPromiseWithoutSendingAnything (the shutdown() loop) and cancelDuringSendNotifiesRemoteAfterTheRequestIsDelivered failed before the change - the first three with the exact NPE / null from the issue, the last one with the cancel message overtaking the request.
  • cancelAfterSendNotifiesRemote and sendFailsOverToTheNextConnectionAndCompletesNormally pin 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 than null; a request cancelled before its send is returned by trySubmitRequest with 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 initial CONNECTOR_INFO request on any failure, including the cancellation from shutdown(), and the retry goes out over the still-open socket before principalIsShuttingDown() removes it. Pre-existing and independent of this change: handshakeComplete() resends the initial CONNECTOR_INFO request while the group is shutting down #125.
  • WebSocketConnectionGroup.principals is a plain TreeSet written from handshake threads and from principalIsShuttingDown() / close() without synchronisation.

…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
@vharseko vharseko added bug Something isn't working framework OpenICF-java-framework java Pull requests that update java code tests Test additions or fixes concurrency Races, locking and thread-safety fixes labels Sep 18, 2026
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 {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working concurrency Races, locking and thread-safety fixes framework OpenICF-java-framework java Pull requests that update java code tests Test additions or fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NullPointerException in WebSocketConnectionGroup.shutdown() when a client is closed while a request is registered but not yet sent

2 participants