Skip to content
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

Release file handles back to caller on failure to take ownership #2715

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

PeterAdams-A
Copy link
Contributor

Motivation:

When taking ownership of input and output file descriptors in a bootstrap the ownership of the file handles remains with the caller in the function taking ownership fails. This is currently where _takingOwnershipOfDescriptors uses NIOFileHandle to take ownership of descriptors but then experiences a later failure.

Modifications:

If an exception is throw in _takingOwnershipOfDescriptors release file descriptors from NIOFileHandle.

Result:

No crash on failure to close file handles before end of scoped usage.

@PeterAdams-A
Copy link
Contributor Author

PeterAdams-A commented May 7, 2024

I can't think of a good way to unit test this. You can force a failure in the right place by passing in an already closed file handle as follows.

let sock = socket(AF_LOCAL, SOCK_STREAM, 0)
close(sock)

let channelFuture = NIOPipeBootstrap(group: NIOSingletons.posixEventLoopGroup).takingOwnershipOfDescriptor(inputOutput: sock)

Using a closed file descriptor of course is almost certain to result in a flaky test, especially if run in parallel.
Using numbers of file descriptors which are out of range or have never been used causes failure earlier in the call stack.

@PeterAdams-A
Copy link
Contributor Author

@swift-server-bot test this please

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The good news is that this is testable. The bad news is you need to use the Syscall Abstraction Layer to write the test. This will let you inject the EBADF you want to find, which will therefore also let you ensure the appropriate flow is hit.

Motivation:

Allocation tests are slightly fragile to small changes.

Modifications:

Allow 50 extra allocations over the 1000 cycles.

Result:

Rst tests less flaky
Motivation:

When taking ownership of input and output file descriptors in a bootstrap
the ownership of the file handles remains with the caller in the function
taking ownership fails.  This is currently where _takingOwnershipOfDescriptors
uses NIOFileHandle to take ownership of descriptors but then experiences a later
failure.

Modifications:

If an exception is throw in _takingOwnershipOfDescriptors release file descriptors
from NIOFileHandle.

Result:

No crash on failure to close file handles before end of scoped usage.
@PeterAdams-A
Copy link
Contributor Author

@Lukasa Let me know how you feel about the test I've tried adding. I looked into hooking sys calls but I believe the current sys call hooking doesn't touch this area at all yet. In order to hook the call made directly from PipeChannel I think I'd need to add a hooking mechanism to that which would involved changing the construction of that. Given the failure is from the construction of PipeChannel feels simpler to just throw the exception directly from a hooking factory for that.

Interested to hear your thoughts.

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't hate this testing strategy, but it does need a little cleanup.

protocol NIOPipeBootstrapHooks {
func makePipeChannel(eventLoop: SelectableEventLoop,
inputPipe: NIOFileHandle?,
outputPipe: NIOFileHandle?) throws -> PipeChannel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
outputPipe: NIOFileHandle?) throws -> PipeChannel;
outputPipe: NIOFileHandle?) throws -> PipeChannel

}

let bootstrap = NIOPipeBootstrap(validatingGroup: elg, hooks: NIOPipeBootstrapHooksChannelFail())
XCTAssertNotNil(bootstrap)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
XCTAssertNotNil(bootstrap)
XCTAssertNotNil(bootstrap)

let sock = socket(AF_LOCAL, SOCK_STREAM, 0)
defer {
close(sock)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we're using this socket anywhere.

outputPipe: NIOFileHandle?) throws -> PipeChannel;
}

fileprivate struct DefaultNIOPipeBootstrapHooks : NIOPipeBootstrapHooks {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fileprivate struct DefaultNIOPipeBootstrapHooks : NIOPipeBootstrapHooks {
fileprivate struct DefaultNIOPipeBootstrapHooks: NIOPipeBootstrapHooks {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants