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

Close prime #337

Merged
merged 11 commits into from
Jun 29, 2018
Merged

Close prime #337

merged 11 commits into from
Jun 29, 2018

Conversation

kazu-yamamoto
Copy link
Collaborator

This should fix #329 and #335.

@kazu-yamamoto
Copy link
Collaborator Author

Cc: @winterland1989, @vdukhovni

--
-- This function is not thread-safe. Consider the following senario.
--
-- 1) Thread A acquires a 'Fd' from 'Socket' by 'fdSocket'.

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 is a thread-safety issue with close. Frankly, the issue here is with fdSocket, not close. If you acquire an fdSocket it can stop being valid at any time if other threads manipulate the same Socket.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree with @vdukhovni.
@winterland1989 Do you mind if I move this doc to fdSocket?

Choose a reason for hiding this comment

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

Is there any other operations that will invalidate socket other than close?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. socketToHandle

Choose a reason for hiding this comment

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

Invalidation is not a thread safety issue. If you wanted to make socketFd safer, it would dup(2) the socket, so that the returned file descriptor would remain valid even if the original socket is closed elsewhere. But since a bare file descriptor has no finalizer, the right thing to provide is a clone operation that wraps a socket with a finalizer around a duplicate socket, and then a thread can get a "private" copy of a socket, on which it can call "fdSocket" if that's needed for some reason (rather than using the wrapped socket for all I/O).

Bottom line, close is a fine thread-safe operation, what is not semantically thread-safe is using the same file descriptor in multiple threads, with any of them free to close the descriptor. EVen without closing it is very difficult to ensure that I/O is properly interleaved. Using the same socket in multiple threads is not a common design pattern.

Choose a reason for hiding this comment

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

Well, my purpose is to stop user calling close from another thread, which is common in some timeout code. I'm not particularly cared other use cases since socket is stream based transport layer rather than message based which doesn't make sense when used under multiple threads.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To understand this well, would you show a typical example where close is used in timeout code?

Choose a reason for hiding this comment

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

By using the timer manager from base on unix:

import  GHC.Event

... 
tm <- getSystemTimerManager
registerTimeout tm 1000000 (close socketA)
...

Or use threadDelay on windows:

forkIO $  threadDelay 1000000  >> close socketA

Here close will be called either from the timer manager thread, or a thread created by user. You can argue that the right thing to do here is to use timeout from System.Timeout which use async exception, but what i'd like to do here is to make people aware that above code is unsafe.

Choose a reason for hiding this comment

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

This is a bad application design, but is not a thread-safety issue in close. The close function will not close the wrong file or crash. It behaves correctly when run in multiple threads.

You don't get correct semantics when you close sockets that are being used by a running thread, but that's using the same socket in two threads, and that's generally a problem.

When two threads race to bind, listen or connect the same socket one fails. When two threads race to read the same stream socket they may each see fragments of application level messages (or intersperse fragments of output messages when writing).
One can make a general comment that use of the same socket in multiple threads is unsafe except as noted.

I'd expect accept to be thread-safe, with each thread getting a separate client connection from the same listening socket. And if some sort of "clone" operation were provided, it could provide some measure of safety against close in another thread, but would still be subject to races in send or recv.

Because we don't want to hold any type of lock around a socket while performing I/O operations that can block, such operations look up the file descriptor atomically (at least in 3.0), but then use the descriptor even though it may become invalid, or another thread may perform interleaved operations. This is a basic fact about external handles, and there's no implementation-specific thread-safety caveat here. Don't mess with the the state of a single socket from multiple threads.

There is one pattern that is safe. One thread reads and a different thread writes, but here I'd be tempted to clone the socket so that the reader and writer have separate underlying file descriptors . Of course if the application waits for both threads and then closes the socket when both are done, that's OK too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made a pointer from close's doc to fdSocket's doc.
@winterland1989 Is this acceptable for you?

close :: Socket -> IO ()
close s = invalidateSocket s (\_ -> return ()) $ \oldfd -> do
closeFdWith (void . c_close . fromIntegral) (fromIntegral oldfd)
-- closeFdWith avoids the deadlock of IO manager.
closeFdWith closeFd (toFd oldfd) `E.catch` ignore

Choose a reason for hiding this comment

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

Does this also catch asynchronous exceptions? It would be better if we had a low-level close that returned an "int", and throw an exception in close' and not throw in close, rather than try to catch exceptions here.

Choose a reason for hiding this comment

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

That is use c_close here and ignore the return value!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is use c_close here and ignore the return value!

Ah. You are right. c_close does not throw exceptions!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does this also catch asynchronous exceptions?

What kind of scenario do you think?

Choose a reason for hiding this comment

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

Things like timeouts and thread cancellation are asynchronous timeouts and must not be caught here. Michael Snoyman has a Control.Exception.Safe module that can catch just synchronous exceptions, but there's no need for that here...

http://hackage.haskell.org/package/safe-exceptions-0.1.7.0

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ignore was removed.
We should consider asynchronous exceptions yet, if necessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK. I understand what you want to explain. I hope that you like the current code.

closeFd = void . c_close . fromIntegral

-- | Close the socket. This function throws exceptions if
-- the underlining system call returns errors.

Choose a reason for hiding this comment

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

s/underlining/underlying/

@@ -112,11 +116,53 @@ invalidateSocket (Socket ref _) errorAction normalAction = do

-----------------------------------------------------------------------------

-- | Close the socket. Sending data to or receiving data from closed socket
-- | Close the socket. This function does not throw exceptions even if
-- the underlining system call returns errors.

Choose a reason for hiding this comment

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

s/underlining/underlying/

Copy link

@vdukhovni vdukhovni left a comment

Choose a reason for hiding this comment

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

Looks OK to me, module spelling corrections.

--
-- If multiple threads use the same socket and one uses 'fdSocket' and
-- the other use 'close', unexpected behavior may happen.
-- For more information, please refer to the documentation of 'fdSocket'.

Choose a reason for hiding this comment

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

The issue affects more than just fdSocket and more than just close. In general it is semantically unsafe for two threads to use the same socket, data may be corrupted, or sent to or read from the wrong place. The only exceptions I can think of are:

  • A reader thread can coëxist with a writer thread
  • Multiple threads can call accept to process incoming connections

So I think this sort of advice belongs at the top of the package, not just the close function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. I will.

Choose a reason for hiding this comment

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

yes, that will be nice.

@kazu-yamamoto kazu-yamamoto merged commit 30b7d79 into haskell:master Jun 29, 2018
@kazu-yamamoto kazu-yamamoto deleted the close-prime branch June 29, 2018 04:25
-- number which thread A is holding.
--
-- In this case, it is safer for Thread A to clone 'Fd' by
-- 'System.Posix.IO.dup'.

Choose a reason for hiding this comment

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

Note, that even dup is not safe if the socket is not locked against close between fdSocket and dup. For a safe dup we'd need to provide an atomic variant, that is guaranteed to be duping the unclosed descriptor inside the given socket. You'd need an MVar associated with the socket to implement a safe dup.

The solution is to avoid use of the same socket (other than acceptor or separation of read/write) in multiple threads. All other concurrency models are unsound, and no thread may manipulate a socket concurrently with another thread outside the limited exception cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will add this race condition to the doc.

@eborden eborden mentioned this pull request Jul 7, 2018
5 tasks
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.

close does not throw on ECONNRESET
3 participants