-
Notifications
You must be signed in to change notification settings - Fork 159
NIOSSLHandler: behave sensibly on close(mode: .output)
#428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NIOSSLHandler: behave sensibly on close(mode: .output)
#428
Conversation
267422f to
59cf0df
Compare
4616900 to
416465c
Compare
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| } | ||
|
|
||
| public func channelActive(context: ChannelHandlerContext) { | ||
| context.channel.getOption(ChannelOptions.allowRemoteHalfClosure).whenSuccess { halfClosureAllowed in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think caching this value is semantically quite right: users can change this value during the lifetime of the Channel. We should look it up when we need it.
Similarly, we should try to use the sync options where we can.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| } | ||
|
|
||
|
|
||
| // TODO: how do I test this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With EmbeddedChannel, you can fire the input closed event into the pipeline manually.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| userInboundInputClosedTriggered(context: context) | ||
| default: | ||
| context.fireUserInboundEventTriggered(event) | ||
| // Trigger closure of TCP input as this event has not occured although we are in state .inputClosed already. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code seems to be in the wrong place.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| let channelError: NIOSSLError | ||
| switch self.state { | ||
| case .closed, .idle: | ||
| return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In both of these cases we should probably still send the user event onwards.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| // We flush all outstanding writes once the handshake step is complete and set our state to .outputClosed aftwerwards. | ||
| // This prevents any further writes to this channel. | ||
| if let promise = promise, let closeOutputPromise = self.closeOutputPromise { | ||
| closeOutputPromise.futureResult.cascade(to: promise) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we possibly have a closeOutputPromise if we're not in state closingOutput?
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| } else if let promise = promise { | ||
| self.closeOutputPromise = promise | ||
| self.state = .closingOutput(self.scheduleTimedOutShutdown(context: context)) | ||
| self.doShutdownStep(context: context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems wrong to only do the shutdown if there's a promise.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
|
|
||
| self.flush(context: context) | ||
|
|
||
| self.closeOutputPromise?.futureResult.whenSuccess { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect: if we don't have a promise, this never fires.
This makes me thing that maybe outputClosing isn't necessary at all. It's a sort of immediate transition: we send our CLOSE_NOTIFY and the process is conceptually "done". So maybe this is an atomic transition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I have added two new commits, one removing the .outputClosing state and one removing the closeOutputPromise — let me know what you think!
5ed780a to
41ebe30
Compare
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| self.state = .closing(self.scheduleTimedOutShutdown(context: context)) | ||
| case .unwrapping, .closing: | ||
| case .active, .outputClosed: | ||
| if self.getAllowRemoteHalfClosureFromChannel(context: context) == false { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we'd cache this across the call to doFlushReadData.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| var halfClosureAllowed = false | ||
| if let syncOptions = context.channel.syncOptions { | ||
| let result = try? syncOptions.getOption(ChannelOptions.allowRemoteHalfClosure) | ||
| result.map { halfClosureAllowed = $0 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using map for the side-effect is a bit weird here, it's probably better to use if let.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
|
|
||
| /// Completes the given promise when all outstanding writes have been flushed. | ||
| private func bufferedWritesCascadeFlushPromise( | ||
| to promise: EventLoopPromise<Void>?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this use of the promise is correct. It should be attached to the close(mode: .output) operation. We should fail it if we don't actually issue that call, but we shouldn't succeed it ourselves.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| flushCompletePromise.futureResult.whenComplete { _ in | ||
| self.doShutdownStep(context: context) | ||
| } | ||
| bufferedWritesCascadeFlushPromise(to: flushCompletePromise, context: context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this promise dance is probably unnecessary. We can send closeOutput once we've called flush, even if the write promises haven't completed. Handlers are required to queue close(mode: .output) behind writes.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| completeHandshake(context: context) | ||
| } | ||
| case .unwrapping, .closing, .unwrapped, .closed: | ||
| case .unwrapping, .closing, .unwrapped, .closed, .inputClosed, .outputClosed: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is wrong, .inputClosed and .outputClosed should match active here I think.
|
Hey @Lukasa , I have implemented your requested changes and added the buffering of I still have a couple of open questions for this PR:
Best regards, |
|
@swift-server-bot add to allowlist |
No, scheduled shutdown is waiting for us to get a
Then the partial close fails and the user should upgrade to full close. |
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| let bufferedWrite = self.bufferedWrites.removeFirst() | ||
| bufferedWrite.promise?.fail(reason) | ||
| private func discardBufferedActions(reason: Error) { | ||
| while self.bufferedActions.count > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be more concisely expressed as while let bufferedAction = self.bufferedActions.popFirst().
| } | ||
| return writeSuccessful | ||
| case .closeOutput: | ||
| invokeCloseOutput = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it incorrect reorders a close(mode: .output) with writes that follow it. Once we get close(mode: .output) we should stop iterating the buffered actions and then, after we send the close, discard anything left in the buffer.
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| } catch { | ||
| // We encountered an error, it's cleanup time. Close ourselves down. | ||
| channelClose(context: context, reason: error) | ||
| if case .outputClosed = self.state {} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really weird construction, prefer using a switch.
Modifications: * flush outstanding messages on NIOSSLHandler.close(mode: .output) * change state once NIOSSLHandler.close(mode: .output) is invoked, so that subsequent writes to the channel fail * write integration tests to verify the behaviour
…ctive(.output)" This reverts commit 59cf0df.
Modifications: * send user event "input closed" onwards when in state .closed or .idle * trigger closure of TCP input once we enter the .inputClosed state
Modifications: * in `closeOuput` trigger the shutdown step regardless of the existence of a promise * remove redundant state transition to `.outputClosed`
Modifications: * remove `.closingOutput` state
Modifications: * pass on user inbound event "input closed" to the next `ChannelHandler` event in the event of an error * cache value of `ChannelOptions.allowRemoteHalfClosure` across `doFlushReadData` to avoid races in case the option's value changes while `doFlushReadData` is being executed * change behaviour: pass `closeOutputPromise` on in `context.close(mode: .output, promise: closeOutputPromise`
Modifications:
* `NIOSSLHandler.userInboundInputClosedTriggered`: execute
`fireErrorCaught` before `userInboundEventTriggered(.inputClosed)`
* `func completedAdditionalPeerCertificateVerification` fails with
invalid state when in states `{.input,.ouput}closed`
Modifications: * create `enum BufferedAction` that allows `NIOSSLHandler` to buffer actions instead of just writes * rename `bufferedWrites` -> `bufferdActions` * add `BufferedAction.closeOutput`, which allows us to buffer invocations of `close(mode: .output)` when we are still in non-active modes such as `.handshaking` or `.additionalVerification` -> this means we'll invoke the closing of the output after our handshaking step is completed and all our buffered writes have been flushed
Modifications: * Refactoring * fixed wrong comment * remove `MarkedCircularBuffer.forEachElementUntilMark()` * discard all buffered actions that occured after the `.closeOutput` buffered actions in `NIOSSLHandler.doUnbufferActions()`
Motivation: Previously we did not check the current state when setting the `NIOSSLHandler`'s state to `.inputClosed`. This resulted in an issue where when we were in state `.closing`, then set the state to `.inputClosed`, we would end up with a fatalError in our shutdown / scheduledShutdown as state `.inputClosed` is not a shutting-down state. Modifications: * check current state before setting `state = .inputClosed`, specifically not setting the state to `.inputClosed` when we are already in the process of doing a full closure
494fb31 to
93458ef
Compare
|
@swift-server-bot test this please |
|
@swift-server-bot test this please |
Modifications: * `NIOSSLHandler`: escalate to full closure when both input and output are closed * `NIOSSLIntegrationTest.testCloseModeOutputServerAndClient`: assert that having input and output closed in `NIOSSLHandler` results in full closure * `NIOSSLIntegrationTest.testCloseModeOutputServerAndClient`: assert that if one side closes its output, the other side of the connection closes its input
93458ef to
937b25b
Compare
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| switch targetCompleteState { | ||
| case .outputClosed: | ||
| // No full channel close here. We would expect users to invoke a full close regardless of | ||
| // previously completed half closures. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is out of date now, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing that out! I have updated the comment so that it reflects the current state of our half-closure behaviour 😄
Sources/NIOSSL/NIOSSLHandler.swift
Outdated
| channelClose(context: context, reason: error) | ||
| switch self.state { | ||
| case .outputClosed: | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we suppress error handling in this state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 🔨
Modifications: * update comment on why we don't do a full channel close when we execute `doShutDownStep` for state `.outputClosed` * do not suppress error in `NIOSSLHandler.doUnbufferActions` when in state `.outputClosed`
Motivation
If NIOSSLHandler receives
close(mode: .output) or close(mode: .input), it ignores them, optionally failing their promises. This is weird behaviour. It should do better.Modifications
NIOSSLHandler.close(mode: .output)NIOSSLHandler.close(mode: .output)NIOSSLHandlerwhen used as a server withChannelOptions.Types.AllowRemoteHalfClosureOptionenabled