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

wasi: nonblocking I/O for sockets and pipes on Windows #1579

Merged
merged 11 commits into from
Jul 18, 2023

Conversation

evacchi
Copy link
Contributor

@evacchi evacchi commented Jul 12, 2023

Further addresses #1500.

Further work to improve the support for nonblocking I/O on Windows. This drops the special-casing for stdin, and instead checks if a handle is a named pipe (which includes sockets and other kinds of pipes); moreover, it further improves _select by introducing a proper wrapper for Winsock's select, which is BSD socket's select, and a compatible implementation of FdSet.

Because we need to distinguish between sockets, pipes, and regular files, the FdSet struct is actually wrapping 3 WinSockFdSets, one for each type; the correct bucket is picket by probing the fd (win Handle) when it's passed to the FdSet method (e.g. FdSet.Set(fd)).

I think this can be further simplified, but I think now it is addressing some of the issues with the current implementation. Further work would be working on poll_oneoff and possibly remove File.PollRead() if the _select() implementations are now good enough for all platforms.

Further work to improve the support to nonblocking I/O on Windows.
This drops the special-casing for stdin, and instead checks if
a handle is a named pipe (which includes sockets and other
kinds of pipes); moreover it further improve _select by
introducing a proper wrapper for winsock's select, which
is BSD socket's select, and a compatible implementation of FdSet.

Signed-off-by: Edoardo Vacchi <[email protected]>
@evacchi evacchi marked this pull request as ready for review July 17, 2023 20:15
Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

I'm marking this as approve, knowing that we may be digging a hole and some things may not be tenable to support on all platforms. Emulation via concurrency on windows is a high skill low population thing, and we have the skills (the edo), but should still consciously think through things as POSIX emulation get gnarlier and gnarlier.

internal/platform/fdset_windows.go Outdated Show resolved Hide resolved
internal/sysfs/file_windows.go Show resolved Hide resolved
internal/sysfs/sock_windows.go Outdated Show resolved Hide resolved
internal/sysfs/sock_windows.go Outdated Show resolved Hide resolved
//
// "Upon successful completion, the pselect() or select() function shall modify the objects pointed to by the readfds,
// writefds, and errorfds arguments to indicate which file descriptors are ready for reading, ready for writing,
// or have an error condition pending, respectively, and shall return the total number of ready descriptors in all the output sets"
Copy link
Contributor

Choose a reason for hiding this comment

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

As this is emulation with goroutines, it is worthwhile to explain what is in windows, but not possible to use. Also, any downside such as this implies GOMAXPROCS>1 if that's true. This explanation could be in the RATIONALE.md like "Why do we emulate POSIX select on windows?"

Copy link
Contributor Author

@evacchi evacchi Jul 18, 2023

Choose a reason for hiding this comment

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

💯 on adding a section to RATIONALE. The tl;dr: is that I don't think there is one single API that does everything select does on POSIX, and, because winsock select is blocking under certain conditions we need to spin a goroutine to avoid blocking everything (e.g. I want to check a PIPE vs a socket: I need to peek on the pipe, and select the socket. TBF we could also poll+peek on the socket with select(, ..., timeout:=0) 🤔 )

another important note: TBH even UNIX select is not perfect for our use cases, because it won't honor the clock config of the module; so if you have a "poll 1 sec fd 4", and you implemented it with select, you would end up blocking for literally 1 sec. So if you wanted to do it properly, I think you may have to use some concurrency primitives there as well.

OTOH when this type of I/O is involved I don't even know if it composes with simulated clocks at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

agree on the tension of real clocks and real files. I found myself in that spaghetti before, thanks for clarifying!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I reworked this to use select with a zero timeout, this simplifies testing and handling quite a bit!

Copy link
Contributor

Choose a reason for hiding this comment

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

thank you!

return bytes > 0, err
nsocks, errno = winsock_select(rs, ws, es, duration)
} else {
// Ticker that emits at every pollInterval.
Copy link
Contributor

Choose a reason for hiding this comment

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

I presume this wasn't extracted to a function because the state "rs, ws, es, duration" or whatever else is needed here, isn't easy to create (and unit test). If it isn't or isn't harder, I might suggest extracting this part and testing it directly vs indirectly via selectAllHandles.

Regardless, if you don't mind doing a coverage pass. It feels like selectAllHandles accepts a context param which would allow reaching some edge cases here (e.g. context already done), but not sure if it is or not.. coverage could tell for sure.

Finally, any area where we rely on goroutines can have some subtle issues and we should be careful. In general stateful concurrent code is the sort of last resort, and I understand this may be the only way out for windows (vs say not supported). Maybe use the hammer test (for the high level function) to make sure the windows calls used below don't have any one-at-a-time things that should be locked. That or say too much.. in any case please consider what may be needed about any concurrency issues here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have reworked all this bit and did a coverage pass, adding a few more tests. With the select(timeout:=0) trick there is no longer a user-controlled goroutine, just channels with ticks and timeouts as before.

Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Signed-off-by: Edoardo Vacchi <[email protected]>
Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

Really good work here. I appreciate the tenacity to eliminate a the goroutine.

I'm not going to subject you to a nit pass. if you feel like anything you can put it in another PR or part of something else, because probably this code will be touched again anyway.

}
for fd := 0; fd < _FD_SETSIZE; fd++ {
isSet := fdSet.IsSet(fd)
if fd&0x1 == 0x1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

require.Equals would be nicer


// Zero clears the set.
func (f *WinSockFdSet) Zero() {
if f == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

unless we use things as nil (later re-assign the pointer) I would leave out the receiver == nil thing for internal types

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the receiver can be nil in many places because select accepts fdreads, fdwrites, fdexcepts to be nil. By allowing the receiver to be nil, some error checks can be avoided e.g. f.Count() returning 0 when f == nil, or in this case, zeroing out a nil receiver, which is also useful in select. To be fair, maybe select can be rewritten in a different way so that the nil checks are centralized at the top of the function (maybe replacing nils with empty structs)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what made me look was not all functions check nil ;) Anyway it is cool to know at least some of it is helpful!

//
// "Upon successful completion, the pselect() or select() function shall modify the objects pointed to by the readfds,
// writefds, and errorfds arguments to indicate which file descriptors are ready for reading, ready for writing,
// or have an error condition pending, respectively, and shall return the total number of ready descriptors in all the output sets"
Copy link
Contributor

Choose a reason for hiding this comment

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

thank you!

t.Run("peekNamedPipe should report the correct state of incoming data in the pipe", func(t *testing.T) {
r, w, err := os.Pipe()
require.NoError(t, err)
rh := syscall.Handle(r.Fd())
wh := syscall.Handle(w.Fd())

// Ensure the pipe has data.
// Ensure the pipe has no data.
n, err := peekNamedPipe(rh)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit if errno return as the name errno and then use require.EqualErrno as it has better formatting on fail

@codefromthecrypt codefromthecrypt merged commit 1e0c73d into main Jul 18, 2023
@codefromthecrypt codefromthecrypt deleted the winsock-select branch July 18, 2023 23:30
@evacchi
Copy link
Contributor Author

evacchi commented Jul 19, 2023

will do a follow up for the nits; maybe address poll_oneoff too.

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.

2 participants