-
Notifications
You must be signed in to change notification settings - Fork 86
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
httpServe appears to be interfering with slave-thread #74
Comments
Repro project: https://github.com/bitemyapp/snap-slave-thread-repro/blob/master/src/Main.hs#L34-L55 (lts-2.14) Screenshot: |
Also |
What's the desired behaviour? You want We can do that if necessary, but the repro example doesn't seem kosher to me. Up to this point the idea has been that the server loops forever until it gets Looking at the example: do
...
SlaveThread.fork canary
_ <- try $ httpServe conf site :: IO (Either SomeException ())
cleanup I think the only reason you would get the ctrl-c semantics you want in normal operation is that the main thread is special and uncaught exceptions from there kill the whole program immediately. And really immediately -- child threads don't get gracefully killed in this scenario. Anyways it should probably look like this: do
...
bracket (SlaveThread.fork canary) killThread $ const $ do
void $ try $ httpServe conf site :: IO (Either SomeException ())
cleanup -- cleanup should be bracketed here too, btw! I think if you write it in this style the issue wouldn't occur. |
Greg's bracket solution seems to work for me. |
...and if you want to do it really really correctly, you should wait for the child thread to finish processing before you exit from main --- these days I would use the |
@gregorycollins I would expect This reproduction is not a perfect simulation of what we're doing in our application. In our application, it's doing a SlaveThread.fork from the App init. app :: SnapletInit App App
app = makeSnaplet "Server" "Server" Nothing $ do
...
spawnAgent frequencyInMinutes f = liftIO $ SlaveThread.fork $ forever
(threadDelay (frequencyInMinutes * 1000000 * 60) >>
logExceptions f) Snap preventing Your suggested edit to the example doesn't fix the problem and isn't adaptable to the "real code". I could modify the example to be more like the real thing, but I don't think that'll help. I don't know the threadId of everything I forked via SlaveThread at the top-level where I fire off |
@gregorycollins @mightybyte Would you be willing to make it stop hiding |
@bitemyapp I don't agree that
I think you're misunderstanding the semantics here, read the implementation: if you call Besides, like I said before, I don't think your
httpServe foo `finally` throwIO ThreadKilled |
@gregorycollins I did try the latter, didn't fix it. I'm left with a few possibilities.
So, I think you're right, if my rethrow didn't fix it – changing it won't fix it either. Do you have any ideas as to why a |
I mean, I wouldn't expect for those slave threads to be cleaned up, not unless
|
Also a potential reason why the behaviour is different in ghci: you don't get the main-thread-seppuku "feature" there because the ghci interpreter thread is not the main thread. |
@gregorycollins GHCi does still show the ThreadKilled exception, seems like GHCi shouldn't break that behavior but I don't know what it really does in this case. I'm pretty sure your first item is impossible because it doesn't seem like Snap provides a way to control or configure thread pool management. Second item is ~roughly what I was thinking I would have to do, but I don't have the ability to do this from outside |
Running the snap initializer in a slave-thread itself would mean that threads it directly forked (using SlaveThread.fork) would get cleaned up on exit. Of course, that just means that you'd have to write a latch in the main thread to wait for clean shutdown on ctrl-c there. If the bracket idiom is too much of a PITA you can try ResourceT. |
@gregorycollins I might try SlaveThread forking the httpServe and bracketing on an async value or something so I know when it ended. |
@gregorycollins you kicking this around with me is helping clear up my confusion on this, thank you very much! |
@gregorycollins I tested forking the It seems right now like the most direct way to work around this is to have a manual registry of the task runner threads and harvest those on cleanup. |
Yes, I think you should keep a list of the worker threads. Alternatively if you're forking this thread within a snaplet initializer then we provide onUnload :: IO () -> Initializer b v () You can use this to kill the thread when the snaplet is torn down. |
@gregorycollins this is a good alternative I'll keep in mind. Instead, we're catching exceptions from In fact, there was a problem with using a lifted I had to change it back to I just keep a |
Code using To do this kind of thing properly you have to carefully mask exceptions at the right places and make sure you don't leave a cancellation point that could cause an exception to terminate your computation unexpectedly. This is easy to get wrong and I have definitely gotten it wrong many times in the past. |
@gregorycollins IIUC – I would be wrapping |
More or less: you should bracket wherever you're creating a slave thread using forkIO. E.g. bracket (forkIO slaveThread) killThread $ const $ httpServe ... When using |
@gregorycollins the try isn't for cleanup though, it's so we can log the error if one occurred. Is |
Personally I would wrap the |
@gregorycollins seems sensible, I'll kick that around. Thank you! |
Children aren't getting harvested on ctrl-c in GHCi, can see their output continue even though the web server itself successfully shut down.
I think slave-thread needs 1 a ThreadKilled 2 exception to bubble up to the parent thread (the one in
main
which invokedhttpServe
) in order for it to do its thing.This looks like a good candidate for starting to narrow things down.
I'll try to put together a minimal repro in a bit.
Some context from IRC:
The text was updated successfully, but these errors were encountered: