Repository navigation
Complete client disconnect when the channel closes - #2135
Merged
Merged
Conversation
The ChannelFsm disconnect action in the TCP and WebSocket client transports completed its future from a channelInactive handler that it added to the pipeline from the transport executor. A channel that was already closed never calls a newly added handler, and a closing channel skips one whose handlerAdded has not run yet. disconnect() then waited for the 5 second fallback timeout. This happens when the application calls disconnect() at about the same moment the connection closes, so the ChannelFsm evaluates Disconnect before the ChannelInactive event the close queued. Complete the disconnect from a close future listener instead. Netty notifies it even when it is added after the channel closed, and it runs before channelInactive propagates, which keeps the previous order. The timeout now only closes the channel, and the listener completes the future. Fixes #2134
1.0.3 and 1.0.4 fix cases where the ChannelFsm could leak a channel, get stuck, or end in the wrong state. Events from a previous channel no longer affect a newer connection, connect() and disconnect() requests apply in the order they were made, and disconnect() closes a channel from a connection attempt that is still pending. The public API Milo uses is unchanged. getChannel() now returns a copy of the FSM's future, which the transport already made itself before sending requests. The FSM also reports Disconnecting to Disconnecting transitions after a close, which the transport's Connected-boundary listener ignores.
[Medium impact] The PR appears safe to merge; no actionable defects were found. SummaryCompletes TCP and WebSocket disconnects from the channel’s close future, including when the channel has already closed.
No actionable issues were found. Build and test results were not independently rerun.
|
Contributor
Author
|
/request-license-review |
Workflow run (with attached summary files): |
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 #2134.
Problem
OpcTcpClientTransport.disconnect()can take 5 seconds when the connection closes at about the same moment the application callsdisconnect(). For example, the server drops the connection while the client is shutting down.The ChannelFsm disconnect action completed its future from a
channelInactivehandler that it added to the front of the pipeline. The action runs on the transport executor, not the event loop, so the handler can miss the event:handlerAddedhasn't run on the event loop yet.The action sees a closed channel when the ChannelFsm evaluates
Disconnectbefore theChannelInactiveevent that the close queued.disconnect()then waited for the 5 second fallback timeout.Fix
The action now completes the disconnect from the channel's close future. Netty notifies a close future listener even when it's added after the channel has closed.
The rest of the flow is unchanged. The action still sends
CloseSecureChannel, and the 5 second timeout still closes the channel if the server never does. The timeout now only closes the channel, and the close future listener completes the disconnect in both cases. The listener runs when the channel closes, beforechannelInactivereaches the UASC handlers, so the disconnect still completes first, as it did when the handler sat at the front of the pipeline.OpcWebSocketClientTransporthad the same disconnect action, so it gets the same change.netty-channel-fsm 1.0.4
The second commit updates netty-channel-fsm from 1.0.2 to 1.0.4. It is separate from the fix above. The regression test still fails on 1.0.4 without the Milo change.
1.0.3 closes a channel from a connection attempt that is still pending when
disconnect()is requested, instead of leaking it. 1.0.4 fixes cases where the FSM could get stuck, leak a channel, or end in the wrong state:ConnectSuccessis evaluated is detected, so the FSM reconnects instead of staying Connected with a closed channel.connect()anddisconnect()requests apply in the order they were made. Before,disconnect(),connect(),disconnect()could end Connected and leave a client reconnecting forever.ChannelActionsmethod that throws or returns null no longer leaves the FSM stuck.Milo needs no code changes for it. The public API it uses is unchanged. The 1.0.4 behavior changes that touch Milo are harmless:
getChannel()returns a copy of the FSM's future. The transport already copied it before sending requests.DisconnectingtoDisconnectingtransition after every close. The transport's listener only reports transitions into or out ofConnected, so it ignores them.getChannel()made after a pendingdisconnect()fails with "not connected" instead of returning the channel that is about to close.Tests
Two tests in
OpcTcpTransportTestrun against a real server transport and bounddisconnect()to 2 seconds, well inside the 5 second fallback, so passing shows the channel close completed the disconnect.disconnectCompletesWhenChannelClosedBeforeDisconnectActionRunsgives the transport a single-thread executor and blocks it, callsdisconnect(), closes the channel, then releases the executor. This makes the ChannelFsm run the disconnect action against a closed channel. It timed out on the unfixed code with both netty-channel-fsm 1.0.2 and 1.0.4, and passes in about 3 ms with the fix.disconnectSendsCloseSecureChannelAndCompletesWhenChannelClosescovers the normal path. It watches the raw outbound bytes for aCLOmessage and checks that the channel is closed whendisconnect()completes.The WebSocket transport has no test suite, so its copy of the change is covered by compilation only.
Verification:
mvn -q spotless:applyandmvn -q clean compilemvn -q -pl opc-ua-stack/transport -am testwith the fix on 1.0.2: 1669 stack-core and 167 transport tests, no failuresmvn -q clean verifywith the fix on 1.0.4: all modules pass, including 1203 integration tests and 214 sdk-client tests